singe/singe/videoPlayer.c
2019-12-05 21:30:23 -06:00

558 lines
17 KiB
C

/*
*
* Singe 2
* Copyright (C) 2019 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 2
* 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.
*
*/
#include <SDL2/SDL_mixer.h>
#include <ffms.h>
#include "thirdparty/uthash.h"
#include "util.h"
#include "videoPlayer.h"
#define AUDIO_STREAM_LOW_WATERMARK (32 * 1024)
#define AUDIO_SAMPLE_PREREAD 1024
#define AUDIO_SILENCE_SECONDS 2
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpadded"
typedef struct VideoPlayerS {
int id;
bool playing;
bool resetTime;
byte *audioBuffer;
byte audioSampleBytes;
byte *audioSilenceRaw;
char errMsg[1024];
int volumeLeft;
int volumeRight;
int audioTrack;
int videoTrack;
int frame;
int audioSampleSize;
int mixSampleSize;
int audioSilenceChannel;
int64_t audioBufferSize;
int64_t frameDeltaTime;
int64_t lastFrameTime;
int64_t timestamp;
int64_t audioPosition;
Uint16 audioFormat;
Uint32 lastTickTime;
Uint32 audioSilenceSize;
Mix_Chunk *silenceChunk;
SDL_AudioStream *audioStream;
SDL_Texture *videoTexture;
FFMS_ErrorInfo errInfo;
FFMS_AudioSource *audioSource;
FFMS_VideoSource *videoSource;
const FFMS_AudioProperties *audioProps;
const FFMS_VideoProperties *videoProps;
const FFMS_Frame *propFrame;
const FFMS_TrackTimeBase *videoTimeBase;
const FFMS_Frame *frameData;
const FFMS_FrameInfo *frameInfo;
UT_hash_handle hh;
} VideoPlayerT;
#pragma GCC diagnostic pop
void _dequeueVideoAudio(int channel, void *stream, int len, void *udata);
int FFMS_CC _indexCallBack(int64_t Current, int64_t Total, void *ICPrivate);
static VideoPlayerT *_videoPlayerHash = NULL;
static int _nextId = 0;
static int _mixRate = -1;
static Uint8 _mixChannels = 0;
static SDL_AudioFormat _mixFormat = 0;
void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) {
VideoPlayerT *v = (VideoPlayerT *)udata;
int bytesToCopy = bytes;
int available = SDL_AudioStreamAvailable(v->audioStream);
int remainder = 0;
int bytesRead = 0;
int i = 0;
Sint16 *data = stream;
(void)channel;
// Don't copy more than we have room for
if (bytesToCopy > available) {
bytesToCopy = available;
}
// Ensure we only copy complete samples (Is this needed?)
remainder = bytesToCopy % v->audioSampleSize;
bytesToCopy -= remainder;
// Read audio data
bytesRead = SDL_AudioStreamGet(v->audioStream, stream, bytesToCopy);
if (bytesRead < 0) utilDie("%s", SDL_GetError());
// We do our own volume per channel here in the mixer
if (_mixChannels < 2) {
// Mono output, average volume levels together
Mix_Volume(channel, (int)((float)MIX_MAX_VOLUME * ((float)v->volumeLeft * (float)v->volumeRight / (float)2) * (float)0.01));
} else {
// Stereo output. Assumes MIX_DEFAULT_FORMAT for now.
Mix_Volume(channel, MIX_MAX_VOLUME);
for (i=0; i<bytesRead / 2; i+=2) {
data[i] = (Sint16)((float)data[i] * (float)v->volumeLeft * (float)0.01);
data[i + 1] = (Sint16)((float)data[i] * (float)v->volumeRight * (float)0.01);
}
}
}
int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *ICPrivate) {
static int lastPercent = 0;
int thisPercent = 0;
(void)ICPrivate; // Contains current VideoPlayerT
if ((current == 0) && (total == 0)) {
lastPercent = 0; // Reset
} else {
thisPercent = (int)((double)current / (double)total * 100.0);
if (thisPercent != lastPercent) {
lastPercent = thisPercent;
utilSay("Indexing: %d%%", thisPercent);
//***TODO*** GUI
}
}
return 0;
}
int videoInit(void) {
int channels = _mixChannels;
// Start FFMS
FFMS_Init(0, 0);
// Fetch mixer settings
if (!Mix_QuerySpec(&_mixRate, &_mixFormat, &channels)) utilDie("%s", Mix_GetError());
_mixChannels = (Uint8)channels;
// Volume only works with MIX_DEFAULT_FORMAT
if (_mixFormat != MIX_DEFAULT_FORMAT) utilDie("videoInit: Only MIX_DEFAULT_FORMAT audio is supported.");
return 0;
}
int videoIsPlaying(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoIsPlaying.", playerIndex);
return v->playing;
}
int videoGetFrame(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoGetHeight.", playerIndex);
return v->frame;
}
int videoGetHeight(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoGetHeight.", playerIndex);
return v->propFrame->EncodedHeight;
}
int videoGetWidth(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoGetWidth.", playerIndex);
return v->propFrame->EncodedWidth;
}
int videoGetVolume(int playerIndex, int *leftPercent, int *rightPercent) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoGetVolume.", playerIndex);
if (leftPercent != NULL) *leftPercent = v->volumeLeft;
if (rightPercent != NULL) *rightPercent = v->volumeRight;
return 0;
}
int videoLoad(char *filename, char *indexPath, bool stretchVideo, SDL_Renderer *renderer) {
char indexName[1024];
int pixelFormats[2];
int result = -1;
FFMS_Index *index = NULL;
FFMS_Indexer *indexer = NULL;
VideoPlayerT *v = NULL;
// Create new videoPlayer
v = calloc(1, sizeof(VideoPlayerT));
if (!v) utilDie("Unable to allocate new video player.");
// Set some starting values
v->audioTrack = -1;
v->videoTrack = -1;
v->audioSilenceChannel = -1;
v->playing = false; // Start paused
v->errInfo.Buffer = v->errMsg;
v->errInfo.BufferSize = sizeof(v->errMsg);
v->errInfo.ErrorType = FFMS_ERROR_SUCCESS;
v->errInfo.SubType = FFMS_ERROR_SUCCESS;
// Index file
snprintf(indexName, 1024, "%s%c%s.index", indexPath, utilGetPathSeparator(), utilGetLastPathComponent(filename));
index = FFMS_ReadIndex(indexName, &v->errInfo);
if (index) {
if (FFMS_IndexBelongsToFile(index, filename, &v->errInfo)) {
FFMS_DestroyIndex(index);
index = NULL;
utilSay("Cached index is invalid.");
} else {
utilSay("Loaded cached index.");
}
}
if (!index) {
utilSay("Creating new index.");
indexer = FFMS_CreateIndexer(filename, &v->errInfo);
if (indexer == NULL) utilDie("%s", v->errInfo.Buffer);
FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_AUDIO, 1, 0);
FFMS_TrackTypeIndexSettings(indexer, FFMS_TYPE_VIDEO, 1, 0);
_indexCallBack(0, 0, v);
FFMS_SetProgressCallback(indexer, _indexCallBack, v);
index = FFMS_DoIndexing2(indexer, FFMS_IEH_ABORT, &v->errInfo);
if (index == NULL) utilDie("%s", v->errInfo.Buffer);
if (FFMS_WriteIndex(indexName, index, &v->errInfo)) utilDie("%s", v->errInfo.Buffer);
}
// Find video track
v->videoTrack = FFMS_GetFirstTrackOfType(index, FFMS_TYPE_VIDEO, &v->errInfo);
if (v->videoTrack < 0) utilDie("%s", v->errInfo.Buffer);
v->videoSource = FFMS_CreateVideoSource(filename, v->videoTrack, index, 1, FFMS_SEEK_NORMAL, &v->errInfo);
if (v->videoSource == NULL) utilDie("%s", v->errInfo.Buffer);
// Get video properties
v->videoProps = FFMS_GetVideoProperties(v->videoSource);
v->propFrame = FFMS_GetFrame(v->videoSource, 0, &v->errInfo);
if (v->propFrame == NULL) utilDie("%s", v->errInfo.Buffer);
v->videoTimeBase = FFMS_GetTimeBase(FFMS_GetTrackFromVideo(v->videoSource));
// Set up output video format
pixelFormats[0] = FFMS_GetPixFmt("bgra");
pixelFormats[1] = -1;
if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight, FFMS_RESIZER_BICUBIC, &v->errInfo)) utilDie("%s", v->errInfo.Buffer);
// Find audio track
v->audioTrack = FFMS_GetFirstTrackOfType(index, FFMS_TYPE_AUDIO, &v->errInfo);
if (v->audioTrack < 0) utilDie("%s", v->errInfo.Buffer);
v->audioSource = FFMS_CreateAudioSource(filename, v->audioTrack, index, FFMS_DELAY_FIRST_VIDEO_TRACK, &v->errInfo);
if (v->audioSource == NULL) utilDie("%s", v->errInfo.Buffer);
// Get audio properties
v->audioProps = FFMS_GetAudioProperties(v->audioSource);
// Index is now part of audioSource & videoSource, so release this one
FFMS_DestroyIndex(index);
// Create video texture
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight);
if (v->videoTexture == NULL) utilDie("%s", SDL_GetError());
if (!stretchVideo) {
SDL_RenderSetLogicalSize(renderer, v->propFrame->EncodedWidth, v->propFrame->EncodedHeight);
}
// Determine audio format
switch (v->audioProps->SampleFormat) {
case FFMS_FMT_U8:
v->audioFormat = AUDIO_U8;
v->audioSampleBytes = 1;
break;
case FFMS_FMT_S16:
v->audioFormat = AUDIO_S16SYS;
v->audioSampleBytes = 2;
break;
case FFMS_FMT_S32:
v->audioFormat = AUDIO_S32SYS;
v->audioSampleBytes = 4;
break;
case FFMS_FMT_FLT:
v->audioFormat = AUDIO_F32SYS;
v->audioSampleBytes = 4;
break;
default:
utilDie("Unknown audio sample format.");
break;
}
if (v->audioProps->Channels > 2) utilDie("Only mono and stereo audio are supported.");
// Create audio stream to convert audio to our desired format
v->audioStream = SDL_NewAudioStream(v->audioFormat, (Uint8)v->audioProps->Channels, v->audioProps->SampleRate, _mixFormat, _mixChannels, _mixRate);
if (!v->audioStream) utilDie("%s", SDL_GetError());
// Create a buffer to read audio into before conversion
v->mixSampleSize = SDL_AUDIO_BITSIZE(_mixFormat) / 8 * _mixChannels;
v->audioSampleSize = v->audioSampleBytes * v->audioProps->Channels;
v->audioBufferSize = v->audioSampleSize * AUDIO_SAMPLE_PREREAD;
v->audioBuffer = (byte *)malloc((size_t)v->audioBufferSize * sizeof(byte));
if (!v->audioBuffer) utilDie("Unable to allocate %ld byte audio buffer.", (size_t)v->audioBufferSize * sizeof(byte));
// Create a block of silent audio to overlay with video stream audio
v->audioSilenceSize = (Uint32)(_mixRate * SDL_AUDIO_BITSIZE(_mixFormat) / 8 * AUDIO_SILENCE_SECONDS);
v->audioSilenceRaw = (byte *)calloc(1, (size_t)v->audioSilenceSize * sizeof(byte));
if (!v->audioSilenceRaw) utilDie("Unable to allocate %ld silence buffer.", v->audioSilenceSize);
// Load silent audio
v->silenceChunk = Mix_QuickLoad_RAW(v->audioSilenceRaw, v->audioSilenceSize);
if (!v->silenceChunk) utilDie("%s", Mix_GetError());
// Start silent audio playback & immediately pause it
v->audioSilenceChannel = Mix_PlayChannel(-1, v->silenceChunk, -1);
if (v->audioSilenceChannel < 0) utilDie("%s", Mix_GetError());
// Register effect to provide video stream audio on this channel
Mix_RegisterEffect(v->audioSilenceChannel, _dequeueVideoAudio, NULL, v);
// Default volume, in percent
v->volumeLeft = 100;
v->volumeRight = 100;
/*
utilSay("Frames: %d (%dx%d) Audio Samples: %ld (%d Hz) %d Channel%s",
v->videoProps->NumFrames,
v->propFrame->EncodedWidth,
v->propFrame->EncodedHeight,
v->audioProps->NumSamples,
v->audioProps->SampleRate,
v->audioProps->Channels,
v->audioProps->Channels == 1 ? "" : "s"
);
*/
// Add to player hash
v->id = _nextId;
HASH_ADD_INT(_videoPlayerHash, id, v);
result = _nextId++;
return result;
}
int videoPause(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoPause.", playerIndex);
v->playing = false;
v->resetTime = true;
return 0;
}
int videoPlay(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoPlay.", playerIndex);
v->playing = true;
v->resetTime = true;
return 0;
}
int videoQuit(void) {
VideoPlayerT *v = NULL;
VideoPlayerT *t = NULL;
// Unload any remaining videos
HASH_ITER(hh, _videoPlayerHash, v, t) {
videoUnload(v->id);
}
FFMS_Deinit();
return 0;
}
int videoSeek(int playerIndex, int seekFrame) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoSeek.", playerIndex);
while (seekFrame >= v->videoProps->NumFrames) {
seekFrame -= v->videoProps->NumFrames;
}
while (seekFrame < 0) {
seekFrame += v->videoProps->NumFrames;
}
v->frame = seekFrame;
v->resetTime = true;
return 0;
}
int videoSetVolume(int playerIndex, int leftPercent, int rightPercent) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoSetVolume.", playerIndex);
v->volumeLeft = leftPercent;
v->volumeRight = rightPercent;
return 0;
}
int videoUnload(int playerIndex) {
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoStop.", playerIndex);
Mix_HaltChannel(v->audioSilenceChannel);
Mix_UnregisterEffect(v->audioSilenceChannel, _dequeueVideoAudio);
Mix_FreeChunk(v->silenceChunk);
free(v->audioSilenceRaw);
SDL_FreeAudioStream(v->audioStream);
free(v->audioBuffer);
FFMS_DestroyAudioSource(v->audioSource);
FFMS_DestroyVideoSource(v->videoSource);
SDL_DestroyTexture(v->videoTexture);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-align"
HASH_DEL(_videoPlayerHash, v);
#pragma GCC diagnostic pop
free(v);
return 0;
}
int videoUpdate(int playerIndex, SDL_Texture **texture) {
int result = -1;
int64_t count = 0;
VideoPlayerT *v = NULL;
// Get our player structure
HASH_FIND_INT(_videoPlayerHash, &playerIndex, v);
if (!v) utilDie("No video player at index %d in videoUpdate.", playerIndex);
// Handle video frames (and time)
if ((SDL_GetTicks() - v->lastTickTime >= v->frameDeltaTime) || v->resetTime) {
v->lastTickTime = SDL_GetTicks();
if (v->frameData) {
SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]);
}
*texture = v->videoTexture;
result = v->frame;
v->frameData = FFMS_GetFrame(v->videoSource, v->frame, &v->errInfo);
if (v->frameData == NULL) utilDie("%s", v->errInfo.Buffer);
v->frameInfo = FFMS_GetFrameInfo(FFMS_GetTrackFromVideo(v->videoSource), v->frame);
v->timestamp = (int64_t)((v->frameInfo->PTS * v->videoTimeBase->Num) / (double)v->videoTimeBase->Den); // Convert to milliseconds
v->frameDeltaTime = v->timestamp - v->lastFrameTime;
v->lastFrameTime = v->timestamp;
if (v->playing) {
if (++v->frame >= v->videoProps->NumFrames) {
v->frame = 0;
v->timestamp = 0;
v->resetTime = true;
}
}
if (v->resetTime) {
SDL_AudioStreamClear(v->audioStream);
v->lastTickTime = 0;
v->frameDeltaTime = 0;
v->audioPosition = (int64_t)((double)(v->timestamp * 0.001) * (double)v->audioProps->SampleRate);
v->resetTime = false;
}
}
// Handle audio samples
if ((v->playing) && (SDL_AudioStreamAvailable(v->audioStream) < AUDIO_STREAM_LOW_WATERMARK) && (v->audioPosition < v->audioProps->NumSamples)) {
// Maximum samples we can read at a time
count = AUDIO_SAMPLE_PREREAD;
// Don't read past end of audio data
if (v->audioPosition + count >= v->audioProps->NumSamples) {
count = v->audioProps->NumSamples - v->audioPosition - 1;
}
// Are we reading anything?
if (count > 0) {
// Get audio from video stream
if (FFMS_GetAudio(v->audioSource, v->audioBuffer, v->audioPosition, count, &v->errInfo)) utilDie("%s", v->errInfo.Buffer);
// Feed it to the mixer stream
if (SDL_AudioStreamPut(v->audioStream, v->audioBuffer, (int)(count * v->audioSampleSize)) < 0) utilDie("%s", SDL_GetError());
v->audioPosition += count;
}
}
return result;
}