aboutsummaryrefslogtreecommitdiff
path: root/engines/sword25/fmv/movieplayer.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engines/sword25/fmv/movieplayer.cpp')
-rw-r--r--engines/sword25/fmv/movieplayer.cpp481
1 files changed, 473 insertions, 8 deletions
diff --git a/engines/sword25/fmv/movieplayer.cpp b/engines/sword25/fmv/movieplayer.cpp
index e4b33bdf8c..69b8fe41ec 100644
--- a/engines/sword25/fmv/movieplayer.cpp
+++ b/engines/sword25/fmv/movieplayer.cpp
@@ -33,59 +33,524 @@
*/
#include "sword25/fmv/movieplayer.h"
+#include "sword25/fmv/oggtheora/oggstate.h"
+#include "sword25/fmv/oggtheora/oggstreamstate.h"
+#include "sword25/fmv/oggtheora/vorbisstate.h"
+#include "sword25/fmv/oggtheora/yuvtorgba.h"
+#include "sword25/gfx/graphicengine.h"
+#include "sword25/gfx/panel.h"
+#include "sword25/kernel/kernel.h"
+#include "sword25/sfx/soundengine.h"
+
+#define BS_LOG_PREFIX "MOVIEPLAYER"
+
+#define FLT_EPSILON 1.192092896e-07F /* smallest such that 1.0+FLT_EPSILON != 1.0 */
namespace Sword25 {
-#define BS_LOG_PREFIX "MOVIEPLAYER"
+const int MAX_FRAMES_PER_TICK = 10;
+const int READ_BLOCK_SIZE = 1024 * 40;
+const float MAX_AUDIO_BUFFER_LENGTH = 1.0f;
Service *OggTheora_CreateObject(Kernel *pKernel) {
return new MoviePlayer(pKernel);
}
-MoviePlayer::MoviePlayer(Kernel *pKernel) : Service(pKernel) {
+// -----------------------------------------------------------------------------
+
+inline bool VerifyRequiredServiceAvailability() {
+ char *RequiredServices[] = { "gfx", "sfx", "package" };
+ for (size_t i = 0; i < sizeof(RequiredServices) / sizeof(RequiredServices[0]); ++i) {
+ if (!Kernel::GetInstance()->GetService(RequiredServices[i])) {
+ BS_LOG_ERRORLN("Required service \"%s\" is not active.", RequiredServices[i]);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ParseStreamHeaders(Common::SharedPtr<MovieFile> &File,
+ Common::SharedPtr<OggState> &OggState,
+ Common::SharedPtr<OggStreamState> &TheoraStreamState,
+ Common::SharedPtr<OggStreamState> &VorbisStreamState,
+ Common::SharedPtr<TheoraState> &TheoraState,
+ Common::SharedPtr<VorbisState> &VorbisState,
+ bool &TheoraPresent,
+ bool &VorbisPresent,
+ const Common::String &Filename) {
+ TheoraPresent = false;
+ VorbisPresent = false;
+
+ // Ogg file open; parse the headers
+ // Only interested in Vorbis/Theora streams
+ bool FinishedHeaderParsing = false;
+ while (!FinishedHeaderParsing) {
+ if (File->BufferData(*OggState.get()) == 0) return false;
+
+ ogg_page Page;
+ while(OggState->SyncPageout(&Page) > 0) {
+ // is this a mandated initial header? If not, stop parsing
+ if (!ogg_page_bos(&Page)) {
+ // don't leak the page; get it into the appropriate stream
+ if (TheoraPresent) TheoraStreamState->PageIn(&Page);
+ if (VorbisPresent) VorbisStreamState->PageIn(&Page);
+
+ FinishedHeaderParsing = true;
+ break;
+ }
+
+ Common::SharedPtr<OggStreamState> streamState(new OggStreamState(ogg_page_serialno(&Page)));
+
+ streamState->PageIn(&Page);
+
+ ogg_packet Packet;
+ streamState->PacketOut(&Packet);
+
+ // identify the codec: try theora
+ if (!TheoraPresent && TheoraState->DecodeHeader(&Packet) >= 0) {
+ // it is theora
+ TheoraStreamState = streamState;
+ TheoraPresent = true;
+ } else if (!VorbisPresent && VorbisState->SynthesisHeaderIn(&Packet) >=0) {
+ // it is vorbis
+ VorbisStreamState = streamState;
+ VorbisPresent = true;
+ }
+ }
+ // fall through to non-bos page parsing
+ }
+
+ // we're expecting more header packets.
+ unsigned int TheoraPacketsRead = TheoraPresent ? 1 : 0;
+ unsigned int VorbisPacketsRead = VorbisPresent ? 1 : 0;
+ while ((TheoraPresent && TheoraPacketsRead < 3) || (VorbisPresent && VorbisPacketsRead < 3)) {
+ int ret;
+ ogg_packet Packet;
+
+ // look for further theora headers
+ while(TheoraPresent && (TheoraPacketsRead < 3) && (ret = TheoraStreamState->PacketOut(&Packet))) {
+ if (ret < 0 || TheoraState->DecodeHeader(&Packet)) {
+ BS_LOG_ERRORLN("Error parsing Theora stream headers. Stream is possibly corrupt. (%s)", Filename.c_str());
+ return false;
+ }
+
+ ++TheoraPacketsRead;
+ if (TheoraPacketsRead == 3) break;
+ }
+
+ // look for more vorbis header packets
+ while(VorbisPresent && (VorbisPacketsRead < 3) && (ret = VorbisStreamState->PacketOut(&Packet))) {
+ if (ret < 0 || VorbisState->SynthesisHeaderIn(&Packet)) {
+ BS_LOG_ERRORLN("Error parsing Vorbis stream headers. Stream is possibly corrupt. (%s)", Filename.c_str());
+ return false;
+ }
+
+ ++VorbisPacketsRead;
+ if (VorbisPacketsRead == 3) break;
+ }
+
+ // The header pages/packets will arrive before anything else we care about, or the stream is not obeying spec
+ ogg_page Page;
+ if (OggState->SyncPageout(&Page) > 0) {
+ // demux into the appropriate stream
+ if (TheoraPresent) TheoraStreamState->PageIn(&Page);
+ if (VorbisPresent) VorbisStreamState->PageIn(&Page);
+ } else {
+ if (File->BufferData(*OggState.get()) == 0) {
+ BS_LOG_ERRORLN("End of file while searching for codec headers. (%s)", Filename.c_str());
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+// -----------------------------------------------------------------------------
+
+MoviePlayer::MoviePlayer(Kernel *pKernel) : Service(pKernel),
+ m_SoundHandle(0), _pixels(NULL) {
if (!_RegisterScriptBindings())
BS_LOG_ERRORLN("Script bindings could not be registered.");
else
BS_LOGLN("Script bindings registered.");
+
+ UnloadMovie();
+}
+
+MoviePlayer::~MoviePlayer() {
+ delete _pixels;
}
bool MoviePlayer::LoadMovie(const Common::String &Filename, unsigned int Z) {
+ if (!VerifyRequiredServiceAvailability()) return false;
+
+ UnloadMovie();
+
+ // Alle Objekte die in dieser Funktion erzeugt werden, werden in zunächst lokalen Auto-Pointern gehalten.
+ // Bei erfolgreicher Beendigung dieser Funktion werden sie den objektlokalen Auto-Pointern zugewiesen.
+ // So wird sichergestellt, dass sie korrekt deinitialisiert werden, wenn in dieser Funktion ein Fehler auftritt, denn beim Zerstören der
+ // lokalen Auto-Pointer werden die entsprechenden Destruktoren aufgerufen.
+
+ // Film laden
+ // Für Filmdateien wird das Cachingsystem nicht benutzt, da Filme in der Regel nur ein Mal abgespielt werden.
+ bool Success = false;
+ Common::SharedPtr<MovieFile> File(new MovieFile(Filename, READ_BLOCK_SIZE, Success));
+ if (!Success) return false;
+
+ // States erzeugen für Ogg, Vorbis und Theora, sowie die Ogg und Theora Streams
+ Common::SharedPtr<OggState> OggState(new OggState());
+ Common::SharedPtr<VorbisState> VorbisState(new VorbisState());
+ Common::SharedPtr<TheoraState> TheoraState(new TheoraState());
+
+ Common::SharedPtr<OggStreamState> TheoraStreamState;
+ Common::SharedPtr<OggStreamState> VorbisStreamState;
+
+ if (!ParseStreamHeaders(File, OggState, TheoraStreamState, VorbisStreamState, TheoraState, VorbisState, m_TheoraPresent, m_VorbisPresent, Filename)) return false;
+
+ // Theora-Decoder Initialisieren
+ if (m_TheoraPresent) {
+ TheoraState->DecodeInit();
+
+ const theora_info & TheoraInfo = TheoraState->GetInfo();
+
+ if (TheoraInfo.pixelformat != OC_PF_444 &&
+ TheoraInfo.pixelformat != OC_PF_422 &&
+ TheoraInfo.pixelformat != OC_PF_420) {
+ BS_LOG_ERRORLN("Unknown chroma sampling. (%s)", Filename.c_str());
+ return false;
+ }
+
+ // Ausgabebitmap erstellen
+ GraphicEngine *pGfx = Kernel::GetInstance()->GetGfx();
+ m_OutputBitmap = pGfx->GetMainPanel()->AddDynamicBitmap(TheoraInfo.frame_width, TheoraInfo.frame_height);
+ if (!m_OutputBitmap.IsValid()) {
+ BS_LOG_ERRORLN("Output bitmap for movie playback could not be created.");
+ return false;
+ }
+
+ // Skalierung des Ausgabebitmaps berechnen, so dass es möglichst viel Bildschirmfläche einnimmt.
+ float ScreenToVideoWidth = (float) pGfx->GetDisplayWidth() / (float) m_OutputBitmap->GetWidth();
+ float ScreenToVideoHeight = (float) pGfx->GetDisplayHeight() / (float) m_OutputBitmap->GetHeight();
+ float ScaleFactor = MIN(ScreenToVideoWidth, ScreenToVideoHeight);
+ if (abs(ScaleFactor - 1.0f) < FLT_EPSILON) ScaleFactor = 1.0f;
+ m_OutputBitmap->SetScaleFactor(ScaleFactor);
+
+ // Z-Wert setzen
+ m_OutputBitmap->SetZ(Z);
+
+ // Ausgabebitmap auf dem Bildschirm zentrieren
+ m_OutputBitmap->SetX((pGfx->GetDisplayWidth() - m_OutputBitmap->GetWidth()) / 2);
+ m_OutputBitmap->SetY((pGfx->GetDisplayHeight() - m_OutputBitmap->GetHeight()) / 2);
+
+ // Buffer für die Pixeldaten erstellen
+ delete _pixels;
+ _pixelsSize = TheoraInfo.width * TheoraInfo.height * 4;
+ _pixels = (byte *)malloc(_pixelsSize);
+ assert(_pixels);
+
+ m_VideoEnded = false;
+ }
+
+ // Vorbis-Decoder initialisieren
+ if (m_VorbisPresent) {
+ VorbisState->SynthesisInit();
+ VorbisState->BlockInit();
+ m_AudioBuffer = Common::SharedPtr<AudioBuffer>(new AudioBuffer());
+
+ m_AudioEnded = false;
+ }
+
+ // Keine Kopie, überträgt Besitz der erzeugten Objekte von der Funktion auf das Objekt.
+ m_File = File;
+ m_OggState = OggState;
+ m_TheoraState = TheoraState;
+ m_TheoraStreamState = TheoraStreamState;
+ m_VorbisState = VorbisState;
+ m_VorbisStreamState = VorbisStreamState;
+ m_MovieLoaded = true;
+ m_Timer = 0;
+
return true;
}
bool MoviePlayer::UnloadMovie() {
+ m_MovieLoaded = false;
+ m_Paused = true;
+
+ m_VorbisStreamState.reset();
+ m_VorbisPresent = false;
+ m_VorbisState.reset();
+ if (m_SoundHandle) {
+ Kernel::GetInstance()->GetSfx()->StopSound(m_SoundHandle);
+ m_SoundHandle = 0;
+ }
+ m_AudioEnded = true;
+ m_AudioBuffer.reset();
+
+ m_TheoraStreamState.reset();
+ m_TheoraPresent = false;
+ m_TheoraState.reset();
+ m_VideoEnded = true;
+
+ m_OggState.reset();
+
+ m_File.reset();
+
+ m_StartTime = 0;
+ m_LastFrameTime = 0;
+ m_Timer = 0.0f;
+
+// Common::Array<unsigned char>().swap(m_Pixels);
+ m_OutputBitmap.Erase();
+
return true;
}
bool MoviePlayer::Play() {
- return true;
+ if (m_MovieLoaded) {
+ if (m_Paused) {
+ if (m_SoundHandle) {
+ SoundEngine * SfxPtr = Kernel::GetInstance()->GetSfx();
+ SfxPtr->ResumeSound(m_SoundHandle);
+ }
+ m_Paused = false;
+ }
+
+ return true;
+ } else {
+ BS_LOG_WARNINGLN("Cannot play movie, when no movie is loaded.");
+ return false;
+ }
}
bool MoviePlayer::Pause() {
- return true;
+ if (m_MovieLoaded) {
+ if (m_SoundHandle) {
+ SoundEngine *SfxPtr = Kernel::GetInstance()->GetSfx();
+ SfxPtr->PauseSound(m_SoundHandle);
+ }
+
+ m_Paused = true;
+ return true;
+ } else {
+ BS_LOG_WARNINGLN("Cannot pause movie, when no movie is loaded.");
+ return false;
+ }
}
void MoviePlayer::Update() {
+ if (m_AudioEnded && m_VideoEnded) {
+ m_Paused = true;
+
+ // Falls der Sound noch läuft, muss er jetzt beendet werden.
+ if (m_SoundHandle) {
+ SoundEngine *SfxPtr = Kernel::GetInstance()->GetSfx();
+ SfxPtr->StopSound(m_SoundHandle);
+ m_SoundHandle = 0;
+ }
+ }
+
+ if (m_Paused) return;
+
+ // Timer aktualisieren.
+ // Wird nur genutzt, wenn keine Audiodaten vorhanden sind.
+ m_Timer += Kernel::GetInstance()->GetGfx()->GetSecondaryFrameDuration();
+
+ // Audiodaten dekodieren
+ if (m_VorbisPresent && !m_AudioEnded) DecodeVorbis();
+
+
+ // Videodaten dekodieren
+ if (m_TheoraPresent && !m_VideoEnded) {
+ bool Result = DecodeTheora();
+
+ if (Result) {
+ // YUV Framebuffer holen
+ yuv_buffer YUVBuffer;
+ m_TheoraState->DecodeYUVOut(&YUVBuffer);
+
+ // YUV Bilddaten nach RGBA konvertieren
+ BS_YUVtoRGBA::YUVtoRGBA(YUVBuffer, m_TheoraState->GetInfo(), _pixels, _pixelsSize);
+
+ // RGBA Bilddaten auf das Ausgabebild kopieren, dabei die Postion des Theoraframes innerhalb des dekodierten Frames beachten.
+ const theora_info &TheoraInfo = m_TheoraState->GetInfo();
+ m_OutputBitmap->SetContent(_pixels, _pixelsSize,
+ (TheoraInfo.offset_x + TheoraInfo.width * TheoraInfo.offset_y) * 4,
+ (TheoraInfo.width - TheoraInfo.frame_width) * 4);
+ }
+ }
}
bool MoviePlayer::IsMovieLoaded() {
- return true;
+ return m_MovieLoaded;
}
bool MoviePlayer::IsPaused() {
- return true;
+ return m_Paused;
}
float MoviePlayer::GetScaleFactor() {
- return 1.0f;
+ if (m_MovieLoaded)
+ return m_OutputBitmap->GetScaleFactorX();
+ else
+ return 0;
}
void MoviePlayer::SetScaleFactor(float ScaleFactor) {
+ if (m_MovieLoaded) {
+ m_OutputBitmap->SetScaleFactor(ScaleFactor);
+
+ // Ausgabebitmap auf dem Bildschirm zentrieren
+ GraphicEngine *gfxPtr = Kernel::GetInstance()->GetGfx();
+ m_OutputBitmap->SetX((gfxPtr->GetDisplayWidth() - m_OutputBitmap->GetWidth()) / 2);
+ m_OutputBitmap->SetY((gfxPtr->GetDisplayHeight() - m_OutputBitmap->GetHeight()) / 2);
+ }
}
double MoviePlayer::GetTime() {
- return 1.0;
+ if (m_VorbisPresent) {
+ if (m_SoundHandle) {
+ float time = Kernel::GetInstance()->GetSfx()->GetSoundTime(m_SoundHandle);
+ return time;
+ } else
+ return 0.0f;
+ } else
+ return m_Timer;
}
+// -----------------------------------------------------------------------------
+
+bool MoviePlayer::DecodeTheora() {
+ double MovieTime = GetTime();
+
+ // Check if this frame time has not passed yet. If the frame is late we need
+ // to decode additonal ones and keep looping, since theora at this stage
+ // needs to decode all frames (due to keyframing)
+ // Getting the current time once at the beginning of the function rather than
+ // every time at the beginning of the loop produces the smoothest framerate
+
+ unsigned int FramesDecoded = 0;
+ bool FrameReady = false;
+ while (m_TheoraState->GranuleTime() < MovieTime) {
+ // theora is one in, one out...
+ ogg_packet Packet;
+ if (m_TheoraStreamState->PacketOut(&Packet) > 0) {
+ if (FramesDecoded < MAX_FRAMES_PER_TICK || Packet.granulepos >= MovieTime) {
+ m_TheoraState->DecodePacketIn(&Packet);
+ FrameReady = true;
+ ++FramesDecoded;
+ }
+ } else {
+ if (m_TheoraStreamState->GetPageBufferSize() > 0)
+ m_TheoraStreamState->PageInBufferedPage();
+ else {
+ if (m_File->IsEOF()) {
+ m_VideoEnded = true;
+ break;
+ } else
+ ReadData();
+ }
+ }
+ }
+ m_LastFrameTime = MovieTime;
+
+ return FrameReady;
+}
+
+void MoviePlayer::DecodeVorbis() {
+ // Vorbis-Stream Infos holen.
+ const vorbis_info &Info = m_VorbisState->GetInfo();
+
+ // Maximalgröße des Audiobuffers berechnen.
+ size_t MaxAudioBufferSamples = static_cast<size_t>(Info.channels * Info.rate * MAX_AUDIO_BUFFER_LENGTH);
+
+ // Zwischenspeicher für die Samples.
+ Common::Array<signed short> Samples;
+
+ // Audiobuffer bis zur Maximalgröße füllen, wenn möglich.
+ while (m_AudioBuffer->Size() < MaxAudioBufferSamples) {
+ // Vorhandene Audiodaten auslesen
+ float **PCM;
+ int SampleCount = m_VorbisState->SynthesisPCMout(&PCM);
+
+ // Wenn Audiodaten gelesen werden konnten, werden diese in 16-Bit Sample umgewandelt und in den Audiobuffer geschrieben.
+ if (SampleCount > 0) {
+ // Im Samplezwischenspeicher leeren und genügend Platz für die kommenden Samples reservieren.
+ Samples.reserve(SampleCount);
+ Samples.clear();
+
+ // Samples konvertieren und in die Samplezwischenspeicher schreiben.
+ for (int i = 0; i < SampleCount; ++i) {
+ for (int j = 0; j < Info.channels; ++j) {
+ int SampleValue = static_cast<int>(PCM[j][i] * 32767.0f);
+ Samples.push_back(CLIP(SampleValue, -32700, 32700));
+ }
+ }
+
+ // Daten aus dem Samplezwischenspeicher in den Audiobuffer schreiben.
+ m_AudioBuffer->Push(&Samples[0], Samples.size());
+
+ // Vorbis mitteilen, dass wir alle Samples gelesen haben.
+ m_VorbisState->SynthesisRead(SampleCount);
+ } else {
+ // Wir konnten, keine Audiodaten auslesen, versuchen ein neues Paket zu dekodieren.
+ ogg_packet Packet;
+ if (m_VorbisStreamState->PacketOut(&Packet) > 0) {
+ if (m_VorbisState->Synthesis(&Packet) == 0) m_VorbisState->SynthesisBlockIn();
+ } else {
+ // Gepufferte Daten in den Stream einfügen.
+ if (m_VorbisStreamState->GetPageBufferSize() > 0)
+ m_VorbisStreamState->PageInBufferedPage();
+ else {
+ // Nicht genug Daten vorhanden. Wenn die Datei leer ist und bereits alle Audiodaten gelesen wurden, ist der Audiostream am Ende.
+ // Ansonsten Daten nachladen.
+ if (m_File->IsEOF()) {
+ if (m_AudioBuffer->Size() == 0) {
+ m_AudioEnded = true;
+ }
+
+ break;
+ } else
+ ReadData();
+ }
+ }
+ }
+ }
+
+ // Soundkanal abspielen, wenn er noch nicht spielt und Audiodaten vorhanden sind.
+ if (m_SoundHandle == 0 && m_AudioBuffer->Size()) {
+ SoundEngine *SfxPtr = Kernel::GetInstance()->GetSfx();
+ m_SoundHandle = SfxPtr->PlayDynamicSoundEx(&DynamicSoundCallBack, this, SoundEngine::SFX, Info.rate, 16, Info.channels);
+ }
+}
+
+void MoviePlayer::ReadData() {
+ if (!m_File->IsEOF()) m_File->BufferData(*m_OggState.get());
+
+ ogg_page Page;
+ while(m_OggState->SyncPageout(&Page) > 0) {
+ if (m_TheoraPresent) m_TheoraStreamState->BufferPage(&Page);
+ if (m_VorbisPresent) m_VorbisStreamState->BufferPage(&Page);
+ }
+}
+
+void MoviePlayer::DynamicSoundCallBack(void *UserData, void *Data, unsigned int DataLength) {
+ MoviePlayer &t = *reinterpret_cast<MoviePlayer *>(UserData);
+
+ signed short *Buffer = reinterpret_cast<signed short *>(Data);
+
+ // Audiodaten in den Soundbuffer schreiben.
+ DataLength -= t.m_AudioBuffer->Pop(Buffer, DataLength / 2) * 2;
+
+ // Falls nicht genug Audiodaten vorhanden waren, wird der Rest mit Stille überschrieben.
+ if (DataLength) {
+ char *ByteBuffer = reinterpret_cast<char *>(Buffer);
+ while (DataLength--) {
+ *ByteBuffer++ = 0;
+ }
+ }
+}
} // End of namespace Sword25