singe/src/videoPlayer.c

1743 lines
59 KiB
C

/*
*
* Singe 3
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
*/
// Frame exact video playback on libavformat and libavcodec.
//
// A laserdisc game addresses video by frame number, so the player needs to know every frame's
// presentation time before it plays anything. The first load of a file demuxes it once without
// decoding, records each video packet's timestamp and keyframe flag, sorts them into display
// order, and caches the table next to the game's other data. Seeking to frame N then means:
// find the last keyframe at or before N, seek the demuxer there, and decode forward until the
// frame whose timestamp matches N comes out. Decoding runs on a thread per player; audio is
// demuxed and decoded separately on the main thread and fed to an SDL3_mixer track.
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#ifdef __linux__
#include <dlfcn.h>
#endif
#include <SDL3/SDL.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include <libavutil/hwcontext.h>
#include <libavutil/imgutils.h>
#include <libavutil/pixdesc.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
#include "../thirdparty/uthash/src/uthash.h"
#include "util.h"
#include "vfs.h"
#include "videoPlayer.h"
// This is kinda ugly but it lets us use the language
// data from VLC without changing their files.
#define VLC_API
#define N_(x) x
typedef struct iso639_lang_t iso639_lang_t;
#include "../thirdparty/vlc/vlc_iso_lang.h"
#include "../thirdparty/vlc/iso-639_def.h"
#undef N_
#undef VLC_API
#define AUDIO_CHANNELS_OUT 2 // Everything is mixed as stereo
#define AUDIO_DRAIN_MS 400 // Long enough for any sane device queue to empty
#define AUDIO_MEASURE_MAX_BUFFERS 64
#define AUDIO_MEASURE_TIMEOUT_MS 2000
#define AUDIO_STREAM_LOW_WATERMARK (24 * 1024) // Bytes queued for the mixer before we stop decoding ahead
#define AVIO_BUFFER_BYTES (64 * 1024) // libavformat's read buffer over a vfs stream
#define BYTES_PER_PIXEL 4
#define BYTES_PER_SAMPLE 4 // Float samples
#define DEFAULT_FPS_NUMERATOR 30
#define DEFAULT_FPS_DENOMINATOR 1
#define ERROR_BUFFER_SIZE 1024
#define INDEX_MAGIC "SINGEIDX"
#define INDEX_VERSION 1
#define KEYFRAME_WARN_SECONDS 2.0 // Seeks decode forward from the previous keyframe
#define LANGUAGE_CODE_LENGTH 3
#define MS_PER_SECOND 1000.0
#define PERCENT_MAX 100
#define PERCENT_TO_SCALE 0.01f
#define PLANE_COUNT 3 // Y, U, V
#define SCALER_PLANES 4 // libswscale reads four plane pointers and strides whatever the format
#define SEEK_FORWARD_LIMIT 64 // Decode forward rather than seek when the wanted frame is this close
#define SEEK_RETRY_MAX 3 // Keyframes to back up when a seek lands past its target
#define STRIDE_ALIGNMENT 64 // libswscale stores with aligned vector instructions; rows must start aligned
// One video frame in display order.
typedef struct FrameInfoS {
int64_t pts; // In the video stream's time base
bool keyframe;
} FrameInfoT;
// What the index cache file starts with.
typedef struct IndexHeaderS {
char magic[8];
int32_t version;
int32_t streamIndex;
int64_t fileSize;
int64_t fileTime;
int64_t count;
} IndexHeaderT;
// One decoded frame owned by the player.
typedef struct FrameBufferS {
uint8_t *data[SCALER_PLANES]; // Only PLANE_COUNT are ever allocated; the rest stay NULL for libswscale
int32_t linesize[SCALER_PLANES];
int64_t frame; // -1 until something has been decoded into it
} FrameBufferT;
typedef struct AudioTrackS {
int32_t streamIndex;
char *language;
} AudioTrackT;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpadded"
typedef struct VideoPlayerS {
int32_t id;
bool playing;
bool resetTime;
bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU
int32_t width;
int32_t height;
int32_t volumeLeft;
int32_t volumeRight;
AVRational fps;
// Frame table, immutable after load.
FrameInfoT *frames;
int64_t frameCount;
AVRational videoTimeBase;
// Video demuxer and decoder. Owned by the decoder thread once it starts.
AVFormatContext *videoFormat;
AVCodecContext *videoCodec;
AVPacket *videoPacket;
AVFrame *videoFrame;
struct SwsContext *sws;
AVBufferRef *hwDevice; // Hardware decoder context, NULL when decoding in software
AVFrame *hwFrame; // Decoded hardware frame transferred to system memory
enum AVPixelFormat hwPixelFormat; // What the hardware decoder hands back
int32_t videoStream;
int64_t nextDecodeFrame; // Frame the decoder will produce next, -1 when unknown
int64_t seekKeyframe; // Keyframe index of the last seek, for backing up
bool videoDrained;
bool packetPending; // videoPacket holds data the decoder refused (EAGAIN)
bool hwReported; // First hardware frame has been traced
// Audio demuxer, decoder, and resampler. Owned by the main thread.
AVFormatContext *audioFormat;
AVCodecContext *audioCodec;
AVPacket *audioPacket;
AVFrame *audioFrame;
SwrContext *swr;
AudioTrackT *audio;
int32_t audioSourceCount;
int32_t currentAudioTrack;
bool audioEof;
int64_t audioSkipUntilMs; // Samples before this time are dropped after a seek
int64_t audioNextMs; // Where the next decoded audio frame is expected to start
float *audioBuffer;
int32_t audioBufferSamples; // Capacity, in sample frames
SDL_AudioSpec audioSpec; // What the resampler produces
SDL_AudioStream *audioStream; // Feeds the mixer track
MIX_Track *track;
// Clock, driven by what the mixer has pulled from the track.
int64_t startTime; // Video time (ms) at the last play/seek
uint64_t startTicks; // Wall clock (ms) at the last play/seek
int64_t samplesPlayed; // Track frames mixed since the last reset, at trackRate
int32_t trackRate; // Sample rate the mixer pulls this track at
uint64_t lastCallbackTicks;
bool audioClockValid; // A callback has run since the last reset
int64_t frame;
// Frame buffers and the decoder thread.
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_Texture *videoTexture;
SDL_Thread *thread;
SDL_Mutex *lock;
SDL_Condition *wake;
char threadErrMsg[ERROR_BUFFER_SIZE];
UT_hash_handle hh;
} VideoPlayerT;
#pragma GCC diagnostic pop
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer);
static void _alsaSetQuiet(bool quiet);
static int64_t _audioClock(VideoPlayerT *v, uint64_t now);
static void _audioCloseTrack(VideoPlayerT *v);
static void _audioQueueFrame(VideoPlayerT *v);
static void _audioSeek(VideoPlayerT *v, int64_t ms);
static void _audioSelectTrack(VideoPlayerT *v, int32_t track);
static void _audioSetupResampler(VideoPlayerT *v);
static int _avioRead(void *opaque, uint8_t *buffer, int size);
static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
static int _compareFrames(const void *a, const void *b); // qsort callback. Not changing int.
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame);
static bool _decodeFrame(VideoPlayerT *v, int64_t want);
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
static void _feedAudio(VideoPlayerT *v);
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts);
static void _formatClose(AVFormatContext **format);
static AVFormatContext *_formatOpen(const char *filename);
static int64_t _frameTime(VideoPlayerT *v, int64_t frame);
static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller);
static char *_indexFileName(const char *filename, const char *indexPath);
static void _loadAudio(VideoPlayerT *v, const char *filename);
static void _measureDeviceQueue(void *udata, MIX_Mixer *mixer, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder);
static void _openVideo(VideoPlayerT *v, const char *filename);
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected);
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
static void _requestFrame(VideoPlayerT *v);
static void _resetClock(VideoPlayerT *v, uint64_t now);
static void _seekVideo(VideoPlayerT *v, int64_t keyframe);
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats); // libavcodec callback.
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase);
static bool _takeDecodedFrame(VideoPlayerT *v);
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
static void _uploadFrame(VideoPlayerT *v);
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
#ifdef __linux__
// libasound prints its own diagnostics to stderr. SDL loads it, so borrow its error hook.
typedef void (*AlsaErrorHandlerT)(const char *file, int line, const char *function, int err, const char *fmt, ...);
typedef int (*AlsaSetErrorHandlerT)(AlsaErrorHandlerT handler);
static void _alsaQuiet(const char *file, int line, const char *function, int err, const char *fmt, ...);
static void *_alsaLibrary = NULL;
#endif
static VideoPlayerT *_videoPlayerHash = NULL;
static int32_t _nextId = 0;
static MIX_Mixer *_mixer = NULL;
static SDL_AudioSpec _mixSpec; // What the mixer feeds the device
static int64_t _mixLatencyMs = 0; // Time between handing audio to the device and hearing it
static int32_t _audioDelayMs = 0; // Per-game correction on top of the measured latency
static int32_t _audioCalibrationMs = 0; // Per-machine correction, from the calibration screen
static bool _hardwareDecoding = true; // Try the platform decoder before falling back to software
// Startup measurement of the audio device queue (see videoInit).
static bool _measuring = false;
static int32_t _measureCount = 0;
static int64_t _measureFrames = 0;
static int64_t _measurePeriodMs = 0;
static uint64_t _measureLastTicks = 0;
// Planes are allocated the way libavcodec allocates its own: aligned start, aligned stride.
// libswscale writes rows with aligned vector stores and faults on anything less.
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] = FFALIGN(v->width * BYTES_PER_PIXEL, STRIDE_ALIGNMENT);
buffer->data[0] = av_malloc((size_t)buffer->linesize[0] * (size_t)v->height);
if (!buffer->data[0]) {
utilDie("Unable to allocate frame buffer.");
}
} else {
buffer->linesize[0] = FFALIGN(v->width, STRIDE_ALIGNMENT);
buffer->linesize[1] = FFALIGN(chromaWidth, STRIDE_ALIGNMENT);
buffer->linesize[2] = buffer->linesize[1];
buffer->data[0] = av_malloc((size_t)buffer->linesize[0] * (size_t)v->height);
buffer->data[1] = av_malloc((size_t)buffer->linesize[1] * (size_t)chromaHeight);
buffer->data[2] = av_malloc((size_t)buffer->linesize[2] * (size_t)chromaHeight);
if (!buffer->data[0] || !buffer->data[1] || !buffer->data[2]) {
utilDie("Unable to allocate frame buffer.");
}
}
}
#ifdef __linux__
static void _alsaQuiet(const char *file, int line, const char *function, int err, const char *fmt, ...) {
(void)file;
(void)line;
(void)function;
(void)err;
(void)fmt;
}
#endif
// Silences libasound while the device is drained on purpose, so the expected underrun is not reported.
static void _alsaSetQuiet(bool quiet) {
#ifdef __linux__
AlsaSetErrorHandlerT setHandler = NULL;
void *symbol = NULL;
if (quiet) {
_alsaLibrary = dlopen("libasound.so.2", RTLD_NOW);
}
if (_alsaLibrary) {
symbol = dlsym(_alsaLibrary, "snd_lib_error_set_handler");
if (symbol) {
memcpy(&setHandler, &symbol, sizeof(setHandler));
setHandler(quiet ? _alsaQuiet : NULL);
}
if (!quiet) {
dlclose(_alsaLibrary);
_alsaLibrary = NULL;
}
}
#else
(void)quiet;
#endif
}
// 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, uint64_t now) {
int64_t played = 0;
int32_t rate = 0;
uint64_t last = 0;
bool valid = false;
int64_t clock = 0;
MIX_LockMixer(_mixer);
played = v->samplesPlayed;
rate = v->trackRate;
last = v->lastCallbackTicks;
valid = v->audioClockValid;
MIX_UnlockMixer(_mixer);
if (valid && (rate > 0)) {
// Consumed samples, interpolated since the last callback, less the device buffer still queued.
clock = v->startTime + (played * (int64_t)MS_PER_SECOND / rate) + (int64_t)(now - last) - _mixLatencyMs - _audioDelayMs - _audioCalibrationMs;
} else {
// No callback yet: wall clock, offset the same way so the switch over is seamless.
clock = v->startTime + (int64_t)(now - v->startTicks) - _mixLatencyMs - _audioDelayMs - _audioCalibrationMs;
}
if (clock < v->startTime) {
clock = v->startTime;
}
return clock;
}
static void _audioCloseTrack(VideoPlayerT *v) {
if (v->audioCodec) {
avcodec_free_context(&v->audioCodec);
}
if (v->swr) {
swr_free(&v->swr);
}
}
// Resamples one decoded audio frame to the track's format and queues it, dropping whatever lies
// before the seek target.
static void _audioQueueFrame(VideoPlayerT *v) {
AVStream *stream = v->audioFormat->streams[v->audio[v->currentAudioTrack].streamIndex];
int64_t ts = v->audioFrame->best_effort_timestamp;
int64_t startMs = (ts == AV_NOPTS_VALUE) ? v->audioNextMs : _streamTimeToMs(ts, stream->time_base);
int32_t capacity = swr_get_out_samples(v->swr, v->audioFrame->nb_samples);
int32_t produced = 0;
int32_t skip = 0;
uint8_t *out = NULL;
if (capacity < 0) {
utilDie("Unable to size the audio resampler output.");
}
if (capacity > v->audioBufferSamples) {
free(v->audioBuffer);
v->audioBufferSamples = capacity;
v->audioBuffer = malloc((size_t)capacity * AUDIO_CHANNELS_OUT * BYTES_PER_SAMPLE);
if (!v->audioBuffer) {
utilDie("Unable to allocate audio buffer.");
}
}
out = (uint8_t *)v->audioBuffer;
produced = swr_convert(v->swr, &out, capacity, (const uint8_t **)v->audioFrame->extended_data, v->audioFrame->nb_samples);
if (produced < 0) {
utilDie("Audio resampling failed.");
}
// After a seek the first frame usually starts before the target; trim it.
if (startMs < v->audioSkipUntilMs) {
skip = (int32_t)((v->audioSkipUntilMs - startMs) * v->audioSpec.freq / (int64_t)MS_PER_SECOND);
if (skip > produced) {
skip = produced;
}
}
if (produced > skip) {
if (!SDL_PutAudioStreamData(v->audioStream, v->audioBuffer + (size_t)skip * AUDIO_CHANNELS_OUT, (produced - skip) * AUDIO_CHANNELS_OUT * BYTES_PER_SAMPLE)) {
utilDie("%s", SDL_GetError());
}
}
v->audioNextMs = startMs + (int64_t)produced * (int64_t)MS_PER_SECOND / v->audioSpec.freq;
}
// Positions the audio demuxer so the next samples queued are the ones for video time ms.
static void _audioSeek(VideoPlayerT *v, int64_t ms) {
AVStream *stream = v->audioFormat->streams[v->audio[v->currentAudioTrack].streamIndex];
int64_t ts = av_rescale(ms, stream->time_base.den, (int64_t)stream->time_base.num * (int64_t)MS_PER_SECOND);
if (av_seek_frame(v->audioFormat, stream->index, ts, AVSEEK_FLAG_BACKWARD) < 0) {
// Before the first packet, or an unseekable stream: start over.
avformat_seek_file(v->audioFormat, stream->index, INT64_MIN, 0, INT64_MAX, 0);
}
avcodec_flush_buffers(v->audioCodec);
_audioSetupResampler(v);
if (v->audioPacket->data) {
av_packet_unref(v->audioPacket);
}
v->audioSkipUntilMs = ms;
v->audioNextMs = ms;
v->audioEof = false;
}
// Opens the decoder for one audio track and points the mixer track's stream at its output.
static void _audioSelectTrack(VideoPlayerT *v, int32_t track) {
AVStream *stream = v->audioFormat->streams[v->audio[track].streamIndex];
const AVCodec *decoder = avcodec_find_decoder(stream->codecpar->codec_id);
SDL_AudioSpec spec;
SDL_AudioStream *fresh = NULL;
int32_t x = 0;
if (decoder == NULL) {
utilDie("No decoder for audio track %d.", track);
}
_audioCloseTrack(v);
v->audioCodec = avcodec_alloc_context3(decoder);
if (!v->audioCodec || (avcodec_parameters_to_context(v->audioCodec, stream->codecpar) < 0) || (avcodec_open2(v->audioCodec, decoder, NULL) < 0)) {
utilDie("Unable to open audio track %d.", track);
}
v->currentAudioTrack = track;
// Only the chosen track needs demuxing.
for (x = 0; x < (int32_t)v->audioFormat->nb_streams; x++) {
v->audioFormat->streams[x]->discard = (x == stream->index) ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
}
// The resampler always produces interleaved float stereo at the track's own rate.
spec.format = SDL_AUDIO_F32;
spec.channels = AUDIO_CHANNELS_OUT;
spec.freq = v->audioCodec->sample_rate;
if ((v->audioStream == NULL) || (spec.freq != v->audioSpec.freq)) {
fresh = SDL_CreateAudioStream(&spec, &_mixSpec);
if (!fresh) {
utilDie("%s", SDL_GetError());
}
if (!MIX_SetTrackAudioStream(v->track, fresh)) {
utilDie("%s", SDL_GetError());
}
if (v->audioStream) {
SDL_DestroyAudioStream(v->audioStream);
}
v->audioStream = fresh;
}
v->audioSpec = spec;
_audioSetupResampler(v);
}
// (Re)creates the resampler for the current track. Also the cheapest way to flush it after a seek.
static void _audioSetupResampler(VideoPlayerT *v) {
AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;
if (v->swr) {
swr_free(&v->swr);
}
if ((swr_alloc_set_opts2(&v->swr, &stereo, AV_SAMPLE_FMT_FLT, v->audioSpec.freq, &v->audioCodec->ch_layout, v->audioCodec->sample_fmt, v->audioCodec->sample_rate, 0, NULL) < 0) || (swr_init(v->swr) < 0)) {
utilDie("Unable to create the audio resampler.");
}
}
// Loads the frame table from the cache, or demuxes the file once to build it and caches the result.
static int _avioRead(void *opaque, uint8_t *buffer, int size) {
int64_t got = vfsStreamRead((VfsStreamT *)opaque, buffer, size);
return (got <= 0) ? AVERROR_EOF : (int)got;
}
static int64_t _avioSeek(void *opaque, int64_t offset, int whence) {
VfsStreamT *stream = (VfsStreamT *)opaque;
if (whence & AVSEEK_SIZE) {
return vfsStreamSize(stream);
}
return vfsStreamSeek(stream, offset, whence & ~AVSEEK_FORCE);
}
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath) {
AVFormatContext *format = NULL;
AVPacket *packet = NULL;
AVStream *stream = NULL;
FrameInfoT *grown = NULL;
IndexHeaderT header;
int64_t fileSize = 0;
int64_t fileTime = 0;
char *indexName = _indexFileName(filename, indexPath);
int64_t capacity = 0;
int64_t pts = 0;
uint64_t started = SDL_GetTicks();
int64_t lastPts = AV_NOPTS_VALUE;
int64_t duration = 1;
int64_t x = 0;
int64_t kept = 0;
if (!vfsStat(filename, &fileSize, &fileTime)) {
utilDie("Unable to stat %s", filename);
}
memset(&header, 0, sizeof(header));
memcpy(header.magic, INDEX_MAGIC, sizeof(header.magic));
header.version = INDEX_VERSION;
header.streamIndex = v->videoStream;
header.fileSize = fileSize;
header.fileTime = fileTime;
if (_readIndexCache(v, indexName, &header)) {
free(indexName);
return;
}
// Demux the whole file once. Every packet of the video stream is one frame.
format = _formatOpen(filename);
if (format == NULL) {
utilDie("Unable to open %s for indexing.", filename);
}
if (avformat_find_stream_info(format, NULL) < 0) {
utilDie("Unable to read stream information from %s.", filename);
}
stream = format->streams[v->videoStream];
for (x = 0; x < (int64_t)format->nb_streams; x++) {
format->streams[x]->discard = (x == v->videoStream) ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
}
packet = av_packet_alloc();
if (!packet) {
utilDie("Unable to allocate a packet.");
}
if (stream->avg_frame_rate.num > 0) {
duration = av_rescale_q(1, av_inv_q(stream->avg_frame_rate), stream->time_base);
if (duration < 1) {
duration = 1;
}
}
while (av_read_frame(format, packet) >= 0) {
if (packet->stream_index == v->videoStream) {
if (v->frameCount == capacity) {
capacity = (capacity == 0) ? 4096 : capacity * 2;
grown = realloc(v->frames, sizeof(FrameInfoT) * (size_t)capacity);
if (!grown) {
utilDie("Unable to allocate the frame table.");
}
v->frames = grown;
}
// Prefer the presentation time; elementary streams may only have decode times or nothing.
pts = (packet->pts != AV_NOPTS_VALUE) ? packet->pts : packet->dts;
if (pts == AV_NOPTS_VALUE) {
pts = (lastPts == AV_NOPTS_VALUE) ? 0 : lastPts + duration;
}
lastPts = pts;
v->frames[v->frameCount].pts = pts;
v->frames[v->frameCount].keyframe = (packet->flags & AV_PKT_FLAG_KEY) != 0;
v->frameCount++;
}
av_packet_unref(packet);
}
av_packet_free(&packet);
_formatClose(&format);
if (v->frameCount == 0) {
utilDie("%s has no video frames.", filename);
}
// Display order, one entry per timestamp.
qsort(v->frames, (size_t)v->frameCount, sizeof(FrameInfoT), _compareFrames);
for (x = 0; x < v->frameCount; x++) {
if ((kept == 0) || (v->frames[x].pts != v->frames[kept - 1].pts)) {
v->frames[kept++] = v->frames[x];
} else if (v->frames[x].keyframe) {
v->frames[kept - 1].keyframe = true;
}
}
v->frameCount = kept;
header.count = kept;
_writeIndexCache(v, indexName, &header);
utilTrace("Indexed %s: %" PRId64 " frames in %" PRIu64 " ms", filename, kept, SDL_GetTicks() - started);
free(indexName);
}
static int _compareFrames(const void *a, const void *b) {
const FrameInfoT *fa = (const FrameInfoT *)a;
const FrameInfoT *fb = (const FrameInfoT *)b;
return (fa->pts > fb->pts) - (fa->pts < fb->pts);
}
// Puts a decoded frame into our buffer in the player's format, converting only when the decoder's
// output differs (10 bit sources, 4:2:2, hardware formats).
static void _convertFrame(VideoPlayerT *v, FrameBufferT *buffer, const AVFrame *frame) {
enum AVPixelFormat wanted = v->rgb ? AV_PIX_FMT_BGRA : AV_PIX_FMT_YUV420P;
int32_t planes = v->rgb ? 1 : PLANE_COUNT;
int32_t plane = 0;
int32_t rows = 0;
int32_t bytes = 0;
if ((frame->format == wanted) || (!v->rgb && (frame->format == AV_PIX_FMT_YUVJ420P))) {
for (plane = 0; plane < planes; plane++) {
rows = (plane == 0) ? v->height : (v->height + 1) / 2;
bytes = v->rgb ? v->width * BYTES_PER_PIXEL : ((plane == 0) ? v->width : (v->width + 1) / 2);
av_image_copy_plane(buffer->data[plane], buffer->linesize[plane], frame->data[plane], frame->linesize[plane], bytes, rows);
}
} else {
v->sws = sws_getCachedContext(v->sws, frame->width, frame->height, (enum AVPixelFormat)frame->format, v->width, v->height, wanted, SWS_BILINEAR, NULL, NULL, NULL);
if (!v->sws) {
utilDie("Unable to convert video frames.");
}
sws_scale(v->sws, (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height, buffer->data, buffer->linesize);
}
}
// Decodes frame "want" into the back buffer. Runs on the decoder thread.
static bool _decodeFrame(VideoPlayerT *v, int64_t want) {
int64_t keyframe = want;
int64_t index = 0;
int64_t ts = 0;
int32_t retries = 0;
int32_t result = 0;
// Decide between decoding forward and seeking.
while ((keyframe > 0) && !v->frames[keyframe].keyframe) {
keyframe--;
}
if (v->videoDrained || (v->nextDecodeFrame < 0) || (want < v->nextDecodeFrame) || (keyframe > v->nextDecodeFrame) || (want - v->nextDecodeFrame > SEEK_FORWARD_LIMIT)) {
_seekVideo(v, keyframe);
}
for (;;) {
result = avcodec_receive_frame(v->videoCodec, v->videoFrame);
if (result == 0) {
ts = v->videoFrame->best_effort_timestamp;
index = (ts == AV_NOPTS_VALUE) ? v->nextDecodeFrame : _findFrameIndex(v, ts);
if (index == want) {
if (v->hwDevice && (v->videoFrame->format == v->hwPixelFormat)) {
// Pull the picture out of the GPU; it arrives as NV12 and is converted like any other format.
if (av_hwframe_transfer_data(v->hwFrame, v->videoFrame, 0) < 0) {
snprintf(v->threadErrMsg, sizeof(v->threadErrMsg), "Unable to read back a hardware decoded frame.");
v->threadError = true;
av_frame_unref(v->videoFrame);
return false;
}
if (!v->hwReported) {
v->hwReported = true;
utilTrace("Video %d: first hardware frame read back as %s %dx%d", v->id, av_get_pix_fmt_name((enum AVPixelFormat)v->hwFrame->format), v->hwFrame->width, v->hwFrame->height);
}
_convertFrame(v, &v->back, v->hwFrame);
av_frame_unref(v->hwFrame);
} else {
_convertFrame(v, &v->back, v->videoFrame);
}
av_frame_unref(v->videoFrame);
v->nextDecodeFrame = want + 1;
return true;
}
if (index > want) {
av_frame_unref(v->videoFrame);
// The seek landed past the target (sparse cues); back up one keyframe and try again.
if ((retries < SEEK_RETRY_MAX) && (v->seekKeyframe > 0)) {
retries++;
keyframe = v->seekKeyframe - 1;
while ((keyframe > 0) && !v->frames[keyframe].keyframe) {
keyframe--;
}
_seekVideo(v, keyframe);
continue;
}
// Best we can do: show what we have.
v->nextDecodeFrame = index + 1;
return false;
}
// Earlier than wanted: keep going.
av_frame_unref(v->videoFrame);
v->nextDecodeFrame = index + 1;
continue;
}
if (result == AVERROR_EOF) {
v->videoDrained = true;
return false;
}
if (result != AVERROR(EAGAIN)) {
av_strerror(result, v->threadErrMsg, sizeof(v->threadErrMsg));
v->threadError = true;
return false;
}
// The decoder wants data.
if (!v->packetPending) {
result = av_read_frame(v->videoFormat, v->videoPacket);
if (result < 0) {
// End of file: drain the decoder.
avcodec_send_packet(v->videoCodec, NULL);
continue;
}
if (v->videoPacket->stream_index != v->videoStream) {
av_packet_unref(v->videoPacket);
continue;
}
v->packetPending = true;
}
result = avcodec_send_packet(v->videoCodec, v->videoPacket);
if (result == 0) {
av_packet_unref(v->videoPacket);
v->packetPending = false;
} else if (result != AVERROR(EAGAIN)) {
av_strerror(result, v->threadErrMsg, sizeof(v->threadErrMsg));
v->threadError = true;
av_packet_unref(v->videoPacket);
v->packetPending = false;
return false;
}
}
}
// Decodes whatever frame was last requested into the back buffer, then waits for the next request.
// Seeks decode forward from a keyframe, so this keeps long seeks off the main loop.
static int _decoderThread(void *data) {
VideoPlayerT *v = (VideoPlayerT *)data;
int64_t want = -1;
bool got = false;
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_WaitCondition(v->wake, v->lock);
continue;
}
want = v->requestedFrame;
v->requestedFrame = -1;
SDL_UnlockMutex(v->lock);
got = _decodeFrame(v, want);
SDL_LockMutex(v->lock);
if (got) {
v->back.frame = want;
v->backReady = true;
} else if (!v->threadError) {
// Nothing decodable there (past the end, or a seek that could not be satisfied): let the
// main thread ask again for whatever frame it wants next rather than hanging on this one.
v->pendingFrame = -1;
}
}
SDL_UnlockMutex(v->lock);
return 0;
}
// Keeps the mixer track's stream topped up from the current audio track.
static void _feedAudio(VideoPlayerT *v) {
int32_t result = 0;
while (!v->audioEof && (SDL_GetAudioStreamQueued(v->audioStream) < AUDIO_STREAM_LOW_WATERMARK)) {
result = avcodec_receive_frame(v->audioCodec, v->audioFrame);
if (result == 0) {
_audioQueueFrame(v);
av_frame_unref(v->audioFrame);
continue;
}
if (result == AVERROR_EOF) {
v->audioEof = true;
break;
}
if (result != AVERROR(EAGAIN)) {
utilDie("Audio decoding failed.");
}
result = av_read_frame(v->audioFormat, v->audioPacket);
if (result < 0) {
avcodec_send_packet(v->audioCodec, NULL);
continue;
}
if (v->audioPacket->stream_index == v->audio[v->currentAudioTrack].streamIndex) {
if (avcodec_send_packet(v->audioCodec, v->audioPacket) < 0) {
utilDie("Audio decoding failed.");
}
}
av_packet_unref(v->audioPacket);
}
}
// Index of the last frame whose timestamp is at or before pts.
static int64_t _findFrameIndex(VideoPlayerT *v, int64_t pts) {
int64_t low = 0;
int64_t high = v->frameCount - 1;
int64_t mid = 0;
if (pts <= v->frames[0].pts) {
return 0;
}
while (low < high) {
mid = (low + high + 1) / 2;
if (v->frames[mid].pts <= pts) {
low = mid;
} else {
high = mid - 1;
}
}
return low;
}
// Closes a demuxer opened by _formatOpen along with the stream behind it.
static void _formatClose(AVFormatContext **format) {
AVIOContext *io = NULL;
VfsStreamT *stream = NULL;
if (*format == NULL) {
return;
}
io = (*format)->pb;
avformat_close_input(format);
if (io != NULL) {
stream = (VfsStreamT *)io->opaque;
av_freep(&io->buffer);
avio_context_free(&io);
}
vfsStreamClose(stream);
}
// Opens a demuxer over a vfs stream, so packed and loose videos read alike. NULL when the name resolves nowhere.
static AVFormatContext *_formatOpen(const char *filename) {
VfsStreamT *stream = vfsStreamOpen(filename);
AVFormatContext *format = NULL;
AVIOContext *io = NULL;
unsigned char *buffer = NULL;
if (stream == NULL) {
return NULL;
}
buffer = (unsigned char *)av_malloc(AVIO_BUFFER_BYTES);
io = avio_alloc_context(buffer, AVIO_BUFFER_BYTES, 0, stream, _avioRead, NULL, _avioSeek);
format = avformat_alloc_context();
format->pb = io;
format->flags |= AVFMT_FLAG_CUSTOM_IO;
if (avformat_open_input(&format, filename, NULL, NULL) < 0) {
av_freep(&io->buffer);
avio_context_free(&io);
vfsStreamClose(stream);
return NULL;
}
return format;
}
// Presentation time of a frame in milliseconds.
static int64_t _frameTime(VideoPlayerT *v, int64_t frame) {
return _streamTimeToMs(v->frames[frame].pts, v->videoTimeBase);
}
static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller) {
VideoPlayerT *v = NULL;
HASH_FIND_INT(_videoPlayerHash, &playerHandle, v);
if (!v) {
utilDie("No video player at index %d in %s.", playerHandle, caller);
}
return v;
}
// Returns a new string the caller must free.
static char *_indexFileName(const char *filename, const char *indexPath) {
char *name = utilCreateString("%s%c%s.index", indexPath, utilGetPathSeparator(), utilGetLastPathComponent(filename));
utilFixPathSeparators(&name, false);
return name;
}
// Opens the audio side of a file (the video file, or the separate audio file of an old framefile) and
// lists its tracks.
static void _loadAudio(VideoPlayerT *v, const char *filename) {
AVDictionaryEntry *tag = NULL;
int32_t x = 0;
int32_t count = 0;
v->audioFormat = _formatOpen(filename);
if (v->audioFormat == NULL) {
utilDie("Unable to open %s for audio.", filename);
}
if (avformat_find_stream_info(v->audioFormat, NULL) < 0) {
utilDie("Unable to read stream information from %s.", filename);
}
for (x = 0; x < (int32_t)v->audioFormat->nb_streams; x++) {
if (v->audioFormat->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
count++;
}
}
if (count == 0) {
_formatClose(&v->audioFormat);
return;
}
v->audio = (AudioTrackT *)calloc((size_t)count, sizeof(AudioTrackT));
if (!v->audio) {
utilDie("Unable to allocate audio tracks.");
}
for (x = 0; x < (int32_t)v->audioFormat->nb_streams; x++) {
if (v->audioFormat->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
v->audio[v->audioSourceCount].streamIndex = x;
tag = av_dict_get(v->audioFormat->streams[x]->metadata, "language", NULL, 0);
if (tag != NULL) {
v->audio[v->audioSourceCount].language = strdup(tag->value);
}
v->audioSourceCount++;
}
}
v->audioPacket = av_packet_alloc();
v->audioFrame = av_frame_alloc();
if (!v->audioPacket || !v->audioFrame) {
utilDie("Unable to allocate audio decoding buffers.");
}
}
// Post-mix hook used once at startup. After the mixer has been stalled long enough for the device
// to drain, SDL refills the device as fast as it accepts data: a burst of callbacks a few
// microseconds apart, then one per chunk period. The burst is the queue depth between the mixer
// and the speaker, which is what the audio clock has to subtract.
static void _measureDeviceQueue(void *udata, MIX_Mixer *mixer, const SDL_AudioSpec *spec, float *pcm, int32_t samples) {
uint64_t now = SDL_GetTicks();
int64_t frames = samples / spec->channels;
(void)udata;
(void)mixer;
(void)pcm;
if (!_measuring) {
return;
}
if (_measureCount == 0) {
_measurePeriodMs = frames * (int64_t)MS_PER_SECOND / spec->freq;
} else if ((int64_t)(now - _measureLastTicks) >= _measurePeriodMs / 2) {
// First callback paced by the device: the burst is over.
_measuring = false;
return;
}
_measureCount++;
_measureFrames += frames;
_measureLastTicks = now;
}
// Attaches the platform's hardware decoder to the codec context when it offers one for this codec.
// Failure of any step leaves the context decoding in software.
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder) {
static const enum AVHWDeviceType wanted[] = {
#if defined(_WIN32)
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DXVA2,
#elif defined(__APPLE__)
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
#else
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_VDPAU,
#endif
AV_HWDEVICE_TYPE_NONE
};
const AVCodecHWConfig *config = NULL;
int32_t x = 0;
int32_t i = 0;
if (!_hardwareDecoding) {
return;
}
for (x = 0; wanted[x] != AV_HWDEVICE_TYPE_NONE; x++) {
for (i = 0; (config = avcodec_get_hw_config(decoder, i)) != NULL; i++) {
if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) && (config->device_type == wanted[x])) {
if (av_hwdevice_ctx_create(&v->hwDevice, wanted[x], NULL, NULL, 0) < 0) {
utilTrace("Hardware decoding via %s is not available; decoding in software.", av_hwdevice_get_type_name(wanted[x]));
break;
}
v->hwPixelFormat = config->pix_fmt;
v->videoCodec->hw_device_ctx = av_buffer_ref(v->hwDevice);
v->videoCodec->get_format = _selectPixelFormat;
v->videoCodec->opaque = v;
v->hwFrame = av_frame_alloc();
if (!v->hwFrame) {
utilDie("Unable to allocate the hardware frame.");
}
// The GPU does the work; frame threads would only add latency to every seek.
v->videoCodec->thread_count = 1;
utilTrace("Hardware decoding via %s.", av_hwdevice_get_type_name(wanted[x]));
return;
}
}
}
}
// Opens the demuxer and decoder the decoder thread will use.
static void _openVideo(VideoPlayerT *v, const char *filename) {
const AVCodec *decoder = NULL;
AVStream *stream = NULL;
int32_t x = 0;
v->videoFormat = _formatOpen(filename);
if (v->videoFormat == NULL) {
utilDie("Unable to open %s.", filename);
}
if (avformat_find_stream_info(v->videoFormat, NULL) < 0) {
utilDie("Unable to read stream information from %s.", filename);
}
v->videoStream = av_find_best_stream(v->videoFormat, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
if ((v->videoStream < 0) || (decoder == NULL)) {
utilDie("%s has no decodable video.", filename);
}
for (x = 0; x < (int32_t)v->videoFormat->nb_streams; x++) {
v->videoFormat->streams[x]->discard = (x == v->videoStream) ? AVDISCARD_DEFAULT : AVDISCARD_ALL;
}
stream = v->videoFormat->streams[v->videoStream];
v->videoTimeBase = stream->time_base;
v->width = stream->codecpar->width;
v->height = stream->codecpar->height;
v->fps = av_guess_frame_rate(v->videoFormat, stream, NULL);
if ((v->fps.num <= 0) || (v->fps.den <= 0)) {
v->fps.num = DEFAULT_FPS_NUMERATOR;
v->fps.den = DEFAULT_FPS_DENOMINATOR;
}
v->videoCodec = avcodec_alloc_context3(decoder);
if (!v->videoCodec || (avcodec_parameters_to_context(v->videoCodec, stream->codecpar) < 0)) {
utilDie("Unable to set up the video decoder for %s.", filename);
}
v->videoCodec->thread_count = 0; // Let libavcodec pick
_openHardware(v, decoder);
if (avcodec_open2(v->videoCodec, decoder, NULL) < 0) {
utilDie("Unable to open the video decoder for %s.", filename);
}
v->videoPacket = av_packet_alloc();
v->videoFrame = av_frame_alloc();
if (!v->videoPacket || !v->videoFrame) {
utilDie("Unable to allocate video decoding buffers.");
}
v->nextDecodeFrame = -1;
v->seekKeyframe = 0;
}
// Loads a cached frame table if it matches this file. Returns false when the table must be rebuilt.
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected) {
size_t bytes = 0;
char *data = utilReadFile(indexName, &bytes);
IndexHeaderT header;
bool valid = false;
if (data && (bytes >= sizeof(IndexHeaderT))) {
memcpy(&header, data, sizeof(header));
valid = (memcmp(header.magic, expected->magic, sizeof(header.magic)) == 0) && (header.version == expected->version) && (header.streamIndex == expected->streamIndex) && (header.fileSize == expected->fileSize) && (header.fileTime == expected->fileTime) && (header.count > 0) && (bytes == sizeof(IndexHeaderT) + sizeof(FrameInfoT) * (size_t)header.count);
}
if (valid) {
v->frameCount = header.count;
v->frames = malloc(sizeof(FrameInfoT) * (size_t)v->frameCount);
if (!v->frames) {
utilDie("Unable to allocate the frame table.");
}
memcpy(v->frames, data + sizeof(IndexHeaderT), sizeof(FrameInfoT) * (size_t)v->frameCount);
}
free(data);
return valid;
}
// 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) {
int64_t frame = 0;
int64_t lastKeyframe = 0;
int64_t keyframes = 0;
int64_t longestGap = 0;
double seconds = 0.0;
for (frame = 0; frame < v->frameCount; frame++) {
if (v->frames[frame].keyframe) {
if ((keyframes > 0) && (frame - lastKeyframe > longestGap)) {
longestGap = frame - lastKeyframe;
}
lastKeyframe = frame;
keyframes++;
}
}
if (v->frameCount - lastKeyframe > longestGap) {
longestGap = v->frameCount - lastKeyframe;
}
seconds = (double)longestGap * (double)v->fps.den / (double)v->fps.num;
utilTrace("%s: %" PRId64 " frames, %" PRId64 " keyframes, longest gap %" PRId64 " frames (%.1f seconds)", filename, v->frameCount, keyframes, longestGap, seconds);
if (seconds > KEYFRAME_WARN_SECONDS) {
utilTrace("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_SignalCondition(v->wake);
}
SDL_UnlockMutex(v->lock);
}
// Restart the presentation clock at the current frame and realign audio to it.
static void _resetClock(VideoPlayerT *v, uint64_t now) {
v->startTicks = now;
v->startTime = _frameTime(v, v->frame);
v->resetTime = false;
if (v->audioSourceCount > 0) {
MIX_LockMixer(_mixer);
SDL_ClearAudioStream(v->audioStream);
v->samplesPlayed = 0;
v->lastCallbackTicks = now;
v->audioClockValid = false;
MIX_UnlockMixer(_mixer);
// Video and audio share the container's timeline, so the frame's time is the audio's time.
_audioSeek(v, v->startTime);
}
}
// Positions the video demuxer at a keyframe and resets the decoder. Decoder thread only.
static void _seekVideo(VideoPlayerT *v, int64_t keyframe) {
if (av_seek_frame(v->videoFormat, v->videoStream, v->frames[keyframe].pts, AVSEEK_FLAG_BACKWARD) < 0) {
avformat_seek_file(v->videoFormat, v->videoStream, INT64_MIN, 0, INT64_MAX, 0);
keyframe = 0;
}
avcodec_flush_buffers(v->videoCodec);
if (v->packetPending) {
av_packet_unref(v->videoPacket);
v->packetPending = false;
}
// Streams without timestamps are counted from here; the rest are matched by timestamp.
v->nextDecodeFrame = keyframe;
v->seekKeyframe = keyframe;
v->videoDrained = false;
}
// libavcodec asks which of the formats it can produce we want; take the hardware one when offered.
static enum AVPixelFormat _selectPixelFormat(AVCodecContext *codec, const enum AVPixelFormat *formats) {
VideoPlayerT *v = (VideoPlayerT *)codec->opaque;
int32_t x = 0;
for (x = 0; formats[x] != AV_PIX_FMT_NONE; x++) {
if (formats[x] == v->hwPixelFormat) {
return formats[x];
}
}
// The hardware declined this stream: fall back to the decoder's first software format.
utilTrace("Video %d: hardware decoder declined the stream; decoding in software.", v->id);
return formats[0];
}
static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase) {
return av_rescale(ts, (int64_t)timeBase.num * (int64_t)MS_PER_SECOND, timeBase.den);
}
// 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("Video decoding failed: %s", v->threadErrMsg);
}
if (v->backReady) {
temp = v->front;
v->front = v->back;
v->back = temp;
v->backReady = false;
taken = true;
SDL_SignalCondition(v->wake);
}
SDL_UnlockMutex(v->lock);
return taken;
}
// Runs on the mixer thread each time this track's audio is pulled. Feeds the audio clock.
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples) {
VideoPlayerT *v = (VideoPlayerT *)udata;
(void)track;
(void)pcm;
// The mixer pulls a track in the track's own rate and resamples afterwards, so count in that rate.
if (v->trackRate != spec->freq) {
utilTrace("Video %d audio is pulled at %d Hz, %d channels", v->id, spec->freq, spec->channels);
}
v->samplesPlayed += samples / spec->channels;
v->trackRate = spec->freq;
v->lastCallbackTicks = SDL_GetTicks();
v->audioClockValid = true;
}
// 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;
}
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header) {
FILE *out = fopen(indexName, "wb");
if (!out) {
utilSay("Warning: Unable to write %s; the video will be indexed again next time.", indexName);
return;
}
if ((fwrite(header, sizeof(IndexHeaderT), 1, out) != 1) || (fwrite(v->frames, sizeof(FrameInfoT), (size_t)v->frameCount, out) != (size_t)v->frameCount)) {
fclose(out);
unlink(indexName);
utilSay("Warning: Unable to write %s; the video will be indexed again next time.", indexName);
return;
}
fclose(out);
}
int32_t videoGetAudioCalibration(void) {
return _audioCalibrationMs;
}
int32_t videoGetAudioDelay(void) {
return _audioDelayMs;
}
// The device queue measured at startup, in milliseconds.
int32_t videoGetAudioLatency(void) {
return (int32_t)_mixLatencyMs;
}
int32_t videoGetAudioTrack(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack;
}
int32_t videoGetAudioTracks(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetAudioTracks")->audioSourceCount;
}
int64_t videoGetFrame(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetFrame")->frame;
}
int64_t videoGetFrameCount(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetFrameCount")->frameCount;
}
int32_t videoGetHeight(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetHeight")->height;
}
const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetLanguage");
const char *r = "unk"; // Unknown Language
if ((audioTrack < 0) || (audioTrack >= v->audioSourceCount)) {
utilDie("Invalid audio track %d in videoGetLanguage.", audioTrack);
}
if ((v->audio[audioTrack].language != NULL) && (strlen(v->audio[audioTrack].language) == LANGUAGE_CODE_LENGTH)) {
r = v->audio[audioTrack].language;
}
return r;
}
const char *videoGetLanguageDescription(const char *languageCode) {
int32_t i = 0;
if (languageCode == NULL) {
return "Unknown";
}
for (i = 0; p_languages[i].psz_eng_name != NULL; i++) {
if ((utilStricmp(languageCode, p_languages[i].psz_iso639_1) == 0) || (utilStricmp(languageCode, p_languages[i].psz_iso639_2T) == 0) || (utilStricmp(languageCode, p_languages[i].psz_iso639_2B) == 0)) {
return p_languages[i].psz_eng_name;
}
}
return "Unknown";
}
// The mixer every sound and video goes through.
MIX_Mixer *videoGetMixer(void) {
return _mixer;
}
// 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->front.frame < 0) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) {
return false;
}
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 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->rgb || (v->front.frame < 0)) {
return false;
}
*pixels = v->front.data[0];
*pitch = v->front.linesize[0];
return true;
}
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetVolume");
if (leftPercent != NULL) {
*leftPercent = v->volumeLeft;
}
if (rightPercent != NULL) {
*rightPercent = v->volumeRight;
}
}
int32_t videoGetWidth(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetWidth")->width;
}
void videoInit(MIX_Mixer *mixer) {
uint64_t started = 0;
// Fetch mixer settings
_mixer = mixer;
if (!MIX_GetMixerFormat(_mixer, &_mixSpec)) {
utilDie("%s", SDL_GetError());
}
// Measure the device queue. SDL cannot report how much audio sits between the mixer and the
// speaker, but it refills an empty device in a burst, and the burst is that amount. Nothing
// but silence is playing yet, so draining the device is inaudible.
_alsaSetQuiet(true);
MIX_SetPostMixCallback(_mixer, _measureDeviceQueue, NULL);
MIX_LockMixer(_mixer);
_measuring = true;
_measureCount = 0;
_measureFrames = 0;
SDL_Delay(AUDIO_DRAIN_MS);
MIX_UnlockMixer(_mixer);
started = SDL_GetTicks();
while (_measuring && ((SDL_GetTicks() - started) < AUDIO_MEASURE_TIMEOUT_MS)) {
SDL_Delay(1);
}
MIX_SetPostMixCallback(_mixer, NULL, NULL);
_alsaSetQuiet(false);
if (_measuring || (_measureCount < 1) || (_measureCount > AUDIO_MEASURE_MAX_BUFFERS)) {
// Unreadable result: assume the two buffers a double-buffered device holds.
_measuring = false;
_measureFrames = (int64_t)(2.0 * (double)_measurePeriodMs * (double)_mixSpec.freq / MS_PER_SECOND);
_measureCount = 2;
}
_mixLatencyMs = _measureFrames * (int64_t)MS_PER_SECOND / _mixSpec.freq;
utilTrace("Audio device queue: %d buffers, %" PRId64 " frames, %" PRId64 " ms at %d Hz", _measureCount, _measureFrames, _mixLatencyMs, _mixSpec.freq);
}
bool videoIsPlaying(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoIsPlaying")->playing;
}
// The mixer's audio thread runs the sound and video callbacks; hold this to read what they write.
void videoLockAudio(void) {
MIX_LockMixer(_mixer);
}
// 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) {
VideoPlayerT *v = NULL;
SDL_PropertiesID playProps = 0;
// Create new videoPlayer
v = calloc(1, sizeof(VideoPlayerT));
if (!v) {
utilDie("Unable to allocate new video player.");
}
// Set some starting values (everything else is zero from calloc)
v->currentAudioTrack = -1;
v->videoStream = -1;
v->volumeLeft = VIDEO_VOLUME_MAX;
v->volumeRight = VIDEO_VOLUME_MAX;
v->rgb = rgb;
v->requestedFrame = -1;
v->pendingFrame = -1;
v->uploadedFrame = -1;
v->front.frame = -1;
v->back.frame = -1;
// Video: demuxer, frame table, decoder.
_openVideo(v, videoFilename);
_buildFrameTable(v, videoFilename, indexPath);
_reportKeyframes(v, videoFilename);
// Create video texture
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());
}
// Video textures scale smoothly.
SDL_SetTextureScaleMode(v->videoTexture, SDL_SCALEMODE_LINEAR);
// Frame buffers and the decoder thread that fills them
_allocateFrameBuffer(v, &v->front);
_allocateFrameBuffer(v, &v->back);
v->lock = SDL_CreateMutex();
v->wake = SDL_CreateCondition();
if (!v->lock || !v->wake) {
utilDie("%s", SDL_GetError());
}
v->thread = SDL_CreateThread(_decoderThread, "singeDecoder", v);
if (v->thread == NULL) {
utilDie("%s", SDL_GetError());
}
// Audio: a mixer track that never halts, fed from its own demuxer on the main thread.
_loadAudio(v, audioFilename ? audioFilename : videoFilename);
if (v->audioSourceCount > 0) {
v->track = MIX_CreateTrack(_mixer);
if (!v->track) {
utilDie("%s", SDL_GetError());
}
_audioSelectTrack(v, 0);
if (!MIX_SetTrackRawCallback(v->track, _trackMixed, v)) {
utilDie("%s", SDL_GetError());
}
playProps = SDL_CreateProperties();
SDL_SetBooleanProperty(playProps, MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN, false);
if (!MIX_PlayTrack(v->track, playProps)) {
utilDie("%s", SDL_GetError());
}
SDL_DestroyProperties(playProps);
// Paused until the video plays.
MIX_PauseTrack(v->track);
}
// Add to player hash
v->id = _nextId++;
HASH_ADD_INT(_videoPlayerHash, id, v);
return v->id;
}
void videoPause(int32_t playerHandle) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoPause");
v->playing = false;
if (v->audioSourceCount > 0) {
MIX_PauseTrack(v->track);
}
}
void videoPlay(int32_t playerHandle) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoPlay");
v->playing = true;
v->resetTime = true;
if (v->audioSourceCount > 0) {
MIX_ResumeTrack(v->track);
}
}
void videoQuit(void) {
VideoPlayerT *v = NULL;
VideoPlayerT *t = NULL;
// Unload any remaining videos
HASH_ITER(hh, _videoPlayerHash, v, t) {
videoUnload(v->id);
}
}
void videoSeek(int32_t playerHandle, int64_t seekFrame) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSeek");
int64_t count = v->frameCount;
// Wrap into range.
if (count > 0) {
seekFrame = ((seekFrame % count) + count) % count;
} else {
seekFrame = 0;
}
v->frame = seekFrame;
v->resetTime = true;
}
void videoSetAudioCalibration(int32_t milliseconds) {
_audioCalibrationMs = milliseconds;
}
void videoSetAudioDelay(int32_t milliseconds) {
_audioDelayMs = milliseconds;
}
// Chosen before any video loads; existing players keep whatever they opened with.
void videoSetHardwareDecoding(bool enabled) {
_hardwareDecoding = enabled;
}
void videoSetAudioTrack(int32_t playerHandle, int32_t track) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetAudioTrack");
if ((track < 0) || (track >= v->audioSourceCount)) {
utilDie("Invalid audio track %d in videoSetAudioTrack.", track);
}
if (track != v->currentAudioTrack) {
_audioSelectTrack(v, track);
// Drop the queued audio from the old track and realign.
v->resetTime = true;
}
}
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetVolume");
MIX_StereoGains gains;
v->volumeLeft = leftPercent;
v->volumeRight = rightPercent;
if (v->audioSourceCount > 0) {
gains.left = (float)leftPercent * PERCENT_TO_SCALE;
gains.right = (float)rightPercent * PERCENT_TO_SCALE;
MIX_SetTrackStereo(v->track, &gains);
}
}
void videoUnlockAudio(void) {
MIX_UnlockMixer(_mixer);
}
void videoUnload(int32_t playerHandle) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoUnload");
int32_t x = 0;
// Stop the decoder before touching anything it uses.
SDL_LockMutex(v->lock);
v->quitThread = true;
SDL_SignalCondition(v->wake);
SDL_UnlockMutex(v->lock);
SDL_WaitThread(v->thread, NULL);
SDL_DestroyCondition(v->wake);
SDL_DestroyMutex(v->lock);
for (x = 0; x < PLANE_COUNT; x++) {
av_free(v->front.data[x]);
av_free(v->back.data[x]);
}
sws_freeContext(v->sws);
av_frame_free(&v->hwFrame);
av_frame_free(&v->videoFrame);
av_packet_free(&v->videoPacket);
avcodec_free_context(&v->videoCodec);
av_buffer_unref(&v->hwDevice);
_formatClose(&v->videoFormat);
free(v->frames);
SDL_DestroyTexture(v->videoTexture);
if (v->audioSourceCount > 0) {
MIX_DestroyTrack(v->track);
SDL_DestroyAudioStream(v->audioStream);
_audioCloseTrack(v);
av_frame_free(&v->audioFrame);
av_packet_free(&v->audioPacket);
_formatClose(&v->audioFormat);
for (x = 0; x < v->audioSourceCount; x++) {
free(v->audio[x].language);
}
free(v->audio);
free(v->audioBuffer);
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
HASH_DEL(_videoPlayerHash, v);
#pragma GCC diagnostic pop
free(v);
}
// Advances playback to match the audio clock (or the wall clock for silent videos). Returns the frame now on the texture.
int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoUpdate");
uint64_t now = SDL_GetTicks();
int64_t elapsed = 0;
int64_t count = v->frameCount;
int64_t next = 0;
int64_t lastDuration = 0;
if (v->resetTime) {
_resetClock(v, now);
} else {
if (v->playing) {
// Where in the video should we be? Follow the audio when there is any.
if (v->audioSourceCount > 0) {
elapsed = _audioClock(v, now);
} else {
elapsed = (int64_t)(now - v->startTicks) + v->startTime;
}
// Advance to the last frame whose presentation time has arrived.
next = v->frame + 1;
while ((next < count) && (_frameTime(v, next) <= elapsed)) {
v->frame = next;
next++;
}
// Past the end of the last frame? Loop.
if (next >= count) {
lastDuration = (int64_t)(MS_PER_SECOND * (double)v->fps.den / (double)v->fps.num);
if (elapsed >= _frameTime(v, count - 1) + lastDuration) {
v->frame = 0;
_resetClock(v, now);
}
}
}
}
// Hand the decoder the frame we want, and show whatever it has finished.
_requestFrame(v);
if (_takeDecodedFrame(v)) {
_uploadFrame(v);
}
*texture = v->videoTexture;
// Handle audio samples
if (v->playing && (v->audioSourceCount > 0)) {
_feedAudio(v);
}
// Report what is on screen, so the caller redraws when it changes.
return v->uploadedFrame;
}