diff options
Diffstat (limited to 'engines/sword25/fmv/movieplayer.cpp')
-rw-r--r-- | engines/sword25/fmv/movieplayer.cpp | 481 |
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 |