diff --git a/CMakeLists.txt b/CMakeLists.txt index d7fb97a74..4e5bd4d03 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1048,8 +1048,10 @@ set(LOVE_SRC_MODULE_VIDEO_ROOT set(LOVE_SRC_MODULE_VIDEO_THEORA src/modules/video/theora/Video.cpp src/modules/video/theora/Video.h - src/modules/video/theora/VideoStream.cpp - src/modules/video/theora/VideoStream.h + src/modules/video/theora/OggDemuxer.cpp + src/modules/video/theora/OggDemuxer.h + src/modules/video/theora/TheoraVideoStream.cpp + src/modules/video/theora/TheoraVideoStream.h ) set(LOVE_SRC_MODULE_VIDEO diff --git a/src/modules/video/theora/OggDemuxer.cpp b/src/modules/video/theora/OggDemuxer.cpp new file mode 100644 index 000000000..21bbc1e7e --- /dev/null +++ b/src/modules/video/theora/OggDemuxer.cpp @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2006-2016 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#include "OggDemuxer.h" + +namespace love +{ +namespace video +{ +namespace theora +{ + +OggDemuxer::OggDemuxer(love::filesystem::File *file) + : file(file) + , streamInited(false) + , videoSerial(0) +{ + ogg_sync_init(&sync); +} + +OggDemuxer::~OggDemuxer() +{ + if (streamInited) + ogg_stream_clear(&stream); + ogg_sync_clear(&sync); +} + +void OggDemuxer::readPage() +{ + char *syncBuffer = nullptr; + while (ogg_sync_pageout(&sync, &page) != 1) + { + if (syncBuffer && !streamInited && ogg_stream_check(&stream)) + throw love::Exception("Invalid stream"); + + syncBuffer = ogg_sync_buffer(&sync, 8192); + size_t read = file->read(syncBuffer, 8192); + ogg_sync_wrote(&sync, read); + } +} + +bool OggDemuxer::readPacket(ogg_packet &packet, bool mustSucceed) +{ + if (!streamInited) + throw love::Exception("Reading from OggDemuxer before initialization (engine bug)"); + + while (ogg_stream_packetout(&stream, &packet) != 1) + { + do + { + // We need to read another page, but there is none, we're at the end + if (ogg_page_eos(&page) && !mustSucceed) + return true; + + readPage(); + } while (ogg_page_serialno(&page) != videoSerial); + + ogg_stream_pagein(&stream, &page); + } + + return false; +} + +void OggDemuxer::resync() +{ + ogg_sync_reset(&sync); + ogg_sync_pageseek(&sync, &page); + ogg_stream_reset(&stream); +} + +bool OggDemuxer::isEos() const +{ + return ogg_page_eos(&page); +} + +const std::string &OggDemuxer::getFilename() const +{ + return file->getFilename(); +} + +OggDemuxer::StreamType OggDemuxer::determineType() +{ + ogg_packet packet; + if (ogg_stream_packetpeek(&stream, &packet) != 1) + return TYPE_UNKNOWN; + + // Theora + // See https://www.theora.org/doc/Theora.pdf section 6.1 + if (packet.bytes >= 7) { + uint8_t headerType = packet.packet[0]; + if (headerType & 0x80 && std::strncmp((const char*) packet.packet+1, "theora", 6) == 0) + return TYPE_THEORA; + } + + return TYPE_UNKNOWN; +} + +OggDemuxer::StreamType OggDemuxer::findStream() +{ + if (streamInited) + { + file->seek(0); + ogg_stream_clear(&stream); + ogg_sync_reset(&sync); + } + + streamInited = true; + while (true) + { + // If this page isn't at the start of a stream, we've seen all streams + readPage(); + if (!ogg_page_bos(&page)) + break; + + videoSerial = ogg_page_serialno(&page); + ogg_stream_init(&stream, videoSerial); + ogg_stream_pagein(&stream, &page); + + StreamType type = determineType(); + switch(type) + { + case TYPE_THEORA: + return type; + default: + break; + } + + ogg_stream_clear(&stream); + ogg_sync_reset(&sync); + } + + streamInited = false; + ogg_stream_clear(&stream); + ogg_sync_reset(&sync); + + return TYPE_UNKNOWN; +} + +bool OggDemuxer::seek(ogg_packet &packet, double target, std::function getTime) +{ + static const double rewindThreshold = 0.01; + + if (target < rewindThreshold) + { + file->seek(0); + resync(); + readPacket(packet, true); + return true; + } + + double low = 0; + double high = file->getSize(); + + while (high-low > rewindThreshold) + { + // Determine our next binary search position + double pos = (high+low)/2; + file->seek(pos); + + // Break sync + resync(); + + // Read a page + readPage(); + readPacket(packet, false); + if (isEos()) + { + // EOS, so we're definitely past our target (or the target is past + // the end) + high = pos; + + // And a workaround for single-page files: + if (high < rewindThreshold) + { + file->seek(0); + resync(); + readPacket(packet, true); + } + else + continue; + } + + // Now search all packets in this page + int result = -1; + for (int i = 0; i < ogg_page_packets(&page); ++i) + { + if (i > 0) + readPacket(packet, true); + + // Determine if this is the right place + double curTime = getTime(packet.granulepos); + double nextTime = getTime(packet.granulepos+1); + + if (curTime == -1) + continue; // Invalid granule position (magic?) + else if (curTime <= target && nextTime > target) + { + // the current frame should be displaying right now + result = 0; + break; + } + else if (curTime > target) + { + // No need to check the other packets, they're all past + // this one + result = 1; + break; + } + } + + // The sign of result determines the direction + if (result == 0) + break; + else if (result < 0) + low = pos; + else + high = pos; + } + + return true; +} + +} // theora +} // video +} // love diff --git a/src/modules/video/theora/OggDemuxer.h b/src/modules/video/theora/OggDemuxer.h new file mode 100644 index 000000000..f74ffb155 --- /dev/null +++ b/src/modules/video/theora/OggDemuxer.h @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2006-2016 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#ifndef LOVE_VIDEO_THEORA_OGGDEMUXER_H +#define LOVE_VIDEO_THEORA_OGGDEMUXER_H + +// STL +#include + +// LOVE +#include "filesystem/File.h" + +// OGG +#include + +namespace love +{ +namespace video +{ +namespace theora +{ + +class OggDemuxer +{ +public: + enum StreamType + { + TYPE_THEORA, + TYPE_UNKNOWN, + }; + + OggDemuxer(love::filesystem::File *file); + ~OggDemuxer(); + + StreamType findStream(); + bool readPacket(ogg_packet &packet, bool mustSucceed = false); + void resync(); + bool isEos() const; + const std::string &getFilename() const; + bool seek(ogg_packet &packet, double target, std::function getTime); + +private: + StrongRef file; + + ogg_sync_state sync; + ogg_stream_state stream; + ogg_page page; + + bool streamInited; + int videoSerial; + + void readPage(); + StreamType determineType(); +}; // OggDemuxer + +} // theora +} // video +} // love + +#endif // LOVE_VIDEO_THEORA_OGGDEMUXER_H diff --git a/src/modules/video/theora/VideoStream.cpp b/src/modules/video/theora/TheoraVideoStream.cpp similarity index 56% rename from src/modules/video/theora/VideoStream.cpp rename to src/modules/video/theora/TheoraVideoStream.cpp index 390e34c27..4410ecb34 100644 --- a/src/modules/video/theora/VideoStream.cpp +++ b/src/modules/video/theora/TheoraVideoStream.cpp @@ -22,7 +22,7 @@ #include // LOVE -#include "VideoStream.h" +#include "TheoraVideoStream.h" using love::filesystem::File; @@ -33,19 +33,18 @@ namespace video namespace theora { -VideoStream::VideoStream(love::filesystem::File *file) - : file(file) +TheoraVideoStream::TheoraVideoStream(love::filesystem::File *file) + : demuxer(file) , headerParsed(false) - , streamInited(false) - , videoSerial(0) , decoder(nullptr) , frameReady(false) , lastFrame(0) , nextFrame(0) - , eos(false) , lagCounter(0) { - ogg_sync_init(&sync); + if (demuxer.findStream() != OggDemuxer::TYPE_THEORA) + throw love::Exception("Invalid video file, video is not theora"); + th_info_init(&videoInfo); frontBuffer = new Frame(); @@ -60,29 +59,24 @@ VideoStream::VideoStream(love::filesystem::File *file) delete backBuffer; delete frontBuffer; th_info_clear(&videoInfo); - ogg_sync_clear(&sync); throw ex; } frameSync.set(new DeltaSync(), Acquire::NORETAIN); } -VideoStream::~VideoStream() +TheoraVideoStream::~TheoraVideoStream() { if (decoder) th_decode_free(decoder); th_info_clear(&videoInfo); - if (headerParsed) - ogg_stream_clear(&stream); - - ogg_sync_clear(&sync); delete frontBuffer; delete backBuffer; } -int VideoStream::getWidth() const +int TheoraVideoStream::getWidth() const { if (headerParsed) return videoInfo.pic_width; @@ -90,7 +84,7 @@ int VideoStream::getWidth() const return 0; } -int VideoStream::getHeight() const +int TheoraVideoStream::getHeight() const { if (headerParsed) return videoInfo.pic_height; @@ -98,72 +92,30 @@ int VideoStream::getHeight() const return 0; } -const std::string &VideoStream::getFilename() const +const std::string &TheoraVideoStream::getFilename() const { - return file->getFilename(); + return demuxer.getFilename(); } -void VideoStream::setSync(FrameSync *frameSync) +void TheoraVideoStream::setSync(FrameSync *frameSync) { love::thread::Lock l(bufferMutex); this->frameSync = frameSync; } -const void *VideoStream::getFrontBuffer() const +const void *TheoraVideoStream::getFrontBuffer() const { return frontBuffer; } -size_t VideoStream::getSize() const +size_t TheoraVideoStream::getSize() const { return sizeof(Frame); } -bool VideoStream::isPlaying() const +bool TheoraVideoStream::isPlaying() const { - return frameSync->isPlaying() && !eos; -} - -void VideoStream::readPage() -{ - char *syncBuffer = nullptr; - while (ogg_sync_pageout(&sync, &page) != 1) - { - if (syncBuffer && !headerParsed && ogg_stream_check(&stream)) - throw love::Exception("Invalid stream"); - - syncBuffer = ogg_sync_buffer(&sync, 8192); - size_t read = file->read(syncBuffer, 8192); - ogg_sync_wrote(&sync, read); - } -} - -bool VideoStream::readPacket(bool mustSucceed) -{ - if (!streamInited) - { - readPage(); - videoSerial = ogg_page_serialno(&page); - ogg_stream_init(&stream, videoSerial); - streamInited = true; - ogg_stream_pagein(&stream, &page); - } - - while (ogg_stream_packetout(&stream, &packet) != 1) - { - do - { - // We need to read another page, but there is none, we're at the end - if (ogg_page_eos(&page) && !mustSucceed) - return eos = true; - - readPage(); - } while (ogg_page_serialno(&page) != videoSerial); - - ogg_stream_pagein(&stream, &page); - } - - return false; + return frameSync->isPlaying() && !demuxer.isEos(); } template @@ -181,7 +133,7 @@ inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y) } } -void VideoStream::parseHeader() +void TheoraVideoStream::parseHeader() { if (headerParsed) return; @@ -191,16 +143,8 @@ void VideoStream::parseHeader() th_comment_init(&comment); int ret; - do - { - readPacket(); - ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); - if (ret == TH_ENOTFORMAT) - { - ogg_stream_clear(&stream); - streamInited = false; - } - } while(ret < 0 && !ogg_page_eos(&page)); + demuxer.readPacket(packet); + ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); if (ret < 0) { @@ -210,7 +154,7 @@ void VideoStream::parseHeader() while (ret > 0) { - readPacket(); + demuxer.readPacket(packet); ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); } @@ -246,111 +190,21 @@ void VideoStream::parseHeader() th_decode_packetin(decoder, &packet, nullptr); } -// Arbitrary seeking isn't supported yet, but rewinding is -void VideoStream::rewind() +void TheoraVideoStream::seekDecoder(double target) { - // Seek our data stream back to the start - file->seek(0); + bool success = demuxer.seek(packet, target, [this](int64 granulepos) { + return th_granule_time(decoder, granulepos); + }); - // Break our sync, and discard the rest of the page - ogg_sync_reset(&sync); - ogg_sync_pageseek(&sync, &page); - - // Read our first page/packet from the stream again - readPacket(true); - - // Now tell theora we're at frame 1 (not 0!) - int64 granPos = 1; - th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &granPos, sizeof(granPos)); - - // Force a redraw, since this will always be less than the sync's position - lastFrame = nextFrame = -1; - eos = false; -} - -void VideoStream::seekDecoder(double target) -{ - const double rewindThreshold = 0.01; - - if (target < rewindThreshold) - { - rewind(); + if (!success) return; - } - - double low = 0; - double high = file->getSize(); - - while (high-low > rewindThreshold) - { - // Determine our next binary search position - double pos = (high+low)/2; - file->seek(pos); - - // Break sync - ogg_sync_reset(&sync); - ogg_sync_pageseek(&sync, &page); - - // Read a page - readPacket(false); - if (eos) - { - // EOS, so we're definitely past our target (or the target is past - // the end) - high = pos; - eos = false; - - // And a workaround for single-page files: - if (high < rewindThreshold) - rewind(); - else - continue; - } - - // Now search all packets in this page - int result = -1; - for (int i = 0; i < ogg_page_packets(&page); ++i) - { - if (i > 0) - readPacket(true); - - // Determine if this is the right place - double curTime = th_granule_time(decoder, packet.granulepos); - double nextTime = th_granule_time(decoder, packet.granulepos+1); - - if (curTime == -1) - continue; // Invalid granule position (magic?) - else if (curTime <= target && nextTime > target) - { - // the current frame should be displaying right now - result = 0; - break; - } - else if (curTime > target) - { - // No need to check the other packets, they're all past - // this one - result = 1; - break; - } - } - - // The sign of result determines the direction - if (result == 0) - break; - else if (result < 0) - low = pos; - else - high = pos; - } // Now update theora and our decoder on this new position of ours lastFrame = nextFrame = -1; - eos = false; th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos)); } -void VideoStream::threadedFillBackBuffer(double dt) +void TheoraVideoStream::threadedFillBackBuffer(double dt) { // Synchronize frameSync->update(dt); @@ -362,7 +216,7 @@ void VideoStream::threadedFillBackBuffer(double dt) // If we're at the end of the stream, or if we're displaying the right frame // stop here - if (eos || position < nextFrame) + if (demuxer.isEos() || position < nextFrame) return; th_ycbcr_buffer bufferinfo; @@ -371,7 +225,7 @@ void VideoStream::threadedFillBackBuffer(double dt) ogg_int64_t granulePosition; do { - if (readPacket()) + if (demuxer.readPacket(packet)) return; } while (th_decode_packetin(decoder, &packet, &granulePosition) != 0); lastFrame = nextFrame; @@ -413,7 +267,7 @@ void VideoStream::threadedFillBackBuffer(double dt) if (position > nextFrame) { if (++lagCounter > 5) - seek(position); + seekDecoder(position); } else lagCounter = 0; @@ -422,14 +276,14 @@ void VideoStream::threadedFillBackBuffer(double dt) frameReady = true; } -void VideoStream::fillBackBuffer() +void TheoraVideoStream::fillBackBuffer() { // Done in worker thread } -bool VideoStream::swapBuffers() +bool TheoraVideoStream::swapBuffers() { - if (eos) + if (demuxer.isEos()) return false; love::thread::Lock l(bufferMutex); diff --git a/src/modules/video/theora/VideoStream.h b/src/modules/video/theora/TheoraVideoStream.h similarity index 84% rename from src/modules/video/theora/VideoStream.h rename to src/modules/video/theora/TheoraVideoStream.h index bd07024f6..63e494a84 100644 --- a/src/modules/video/theora/VideoStream.h +++ b/src/modules/video/theora/TheoraVideoStream.h @@ -27,6 +27,7 @@ #include "common/int.h" #include "filesystem/File.h" #include "thread/threads.h" +#include "OggDemuxer.h" // OGG/Theora #include @@ -40,11 +41,11 @@ namespace video namespace theora { -class VideoStream : public love::video::VideoStream +class TheoraVideoStream : public love::video::VideoStream { public: - VideoStream(love::filesystem::File *file); - ~VideoStream(); + TheoraVideoStream(love::filesystem::File *file); + ~TheoraVideoStream(); const void *getFrontBuffer() const; size_t getSize() const; @@ -61,14 +62,10 @@ public: void threadedFillBackBuffer(double dt); private: - StrongRef file; + OggDemuxer demuxer; bool headerParsed; - bool streamInited; - int videoSerial; - ogg_sync_state sync; - ogg_stream_state stream; - ogg_page page; + ogg_packet packet; th_info videoInfo; @@ -86,15 +83,11 @@ private: double lastFrame; double nextFrame; - bool eos; unsigned int lagCounter; - void readPage(); - bool readPacket(bool mustSucceed = false); // true if eos void parseHeader(); - void rewind(); void seekDecoder(double target); -}; // VideoStream +}; // TheoraVideoStream } // theora } // video diff --git a/src/modules/video/theora/Video.cpp b/src/modules/video/theora/Video.cpp index 86c3b3caa..7e3121128 100644 --- a/src/modules/video/theora/Video.cpp +++ b/src/modules/video/theora/Video.cpp @@ -46,7 +46,7 @@ Video::~Video() VideoStream *Video::newVideoStream(love::filesystem::File *file) { - VideoStream *stream = new VideoStream(file); + TheoraVideoStream *stream = new TheoraVideoStream(file); workerThread->addStream(stream); return stream; } @@ -67,7 +67,7 @@ Worker::~Worker() stop(); } -void Worker::addStream(VideoStream *stream) +void Worker::addStream(TheoraVideoStream *stream) { love::thread::Lock l(mutex); streams.push_back(stream); @@ -110,7 +110,7 @@ void Worker::threadFunction() for (auto it = streams.begin(); it != streams.end(); ++it) { - VideoStream *stream = *it; + TheoraVideoStream *stream = *it; if (stream->getReferenceCount() == 1) { // We're the only ones left diff --git a/src/modules/video/theora/Video.h b/src/modules/video/theora/Video.h index 0d5c81313..eac2691ed 100644 --- a/src/modules/video/theora/Video.h +++ b/src/modules/video/theora/Video.h @@ -28,7 +28,8 @@ #include "filesystem/File.h" #include "video/Video.h" #include "thread/threads.h" -#include "VideoStream.h" +#include "video/VideoStream.h" +#include "TheoraVideoStream.h" namespace love { @@ -63,13 +64,13 @@ public: // Implements Threadable void threadFunction(); - void addStream(VideoStream *stream); + void addStream(TheoraVideoStream *stream); // Frees itself! void stop(); private: - std::vector> streams; + std::vector> streams; love::thread::MutexRef mutex; love::thread::ConditionalRef cond;