Split theora::VideoStream into OggDemuxer and TheoraVideoStream

This makes the internal API more modular, but the split also forces a cleaner
implementation. It also means the OggDemuxer is now responsible for the
ogg-specific stuff like seeking and (ogg-specific) type detection.

Backported from the love.video-vpx branch of love-experiments

--HG--
branch : minor
This commit is contained in:
Bart van Strien
2017-10-12 14:23:58 +02:00
parent 95adf7e347
commit 0e9f71b29a
7 changed files with 369 additions and 200 deletions
+4 -2
View File
@@ -1048,8 +1048,10 @@ set(LOVE_SRC_MODULE_VIDEO_ROOT
set(LOVE_SRC_MODULE_VIDEO_THEORA set(LOVE_SRC_MODULE_VIDEO_THEORA
src/modules/video/theora/Video.cpp src/modules/video/theora/Video.cpp
src/modules/video/theora/Video.h src/modules/video/theora/Video.h
src/modules/video/theora/VideoStream.cpp src/modules/video/theora/OggDemuxer.cpp
src/modules/video/theora/VideoStream.h src/modules/video/theora/OggDemuxer.h
src/modules/video/theora/TheoraVideoStream.cpp
src/modules/video/theora/TheoraVideoStream.h
) )
set(LOVE_SRC_MODULE_VIDEO set(LOVE_SRC_MODULE_VIDEO
+242
View File
@@ -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<double(int64)> 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
+77
View File
@@ -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 <functional>
// LOVE
#include "filesystem/File.h"
// OGG
#include <ogg/ogg.h>
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<double(int64)> getTime);
private:
StrongRef<love::filesystem::File> 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
@@ -22,7 +22,7 @@
#include <iostream> #include <iostream>
// LOVE // LOVE
#include "VideoStream.h" #include "TheoraVideoStream.h"
using love::filesystem::File; using love::filesystem::File;
@@ -33,19 +33,18 @@ namespace video
namespace theora namespace theora
{ {
VideoStream::VideoStream(love::filesystem::File *file) TheoraVideoStream::TheoraVideoStream(love::filesystem::File *file)
: file(file) : demuxer(file)
, headerParsed(false) , headerParsed(false)
, streamInited(false)
, videoSerial(0)
, decoder(nullptr) , decoder(nullptr)
, frameReady(false) , frameReady(false)
, lastFrame(0) , lastFrame(0)
, nextFrame(0) , nextFrame(0)
, eos(false)
, lagCounter(0) , 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); th_info_init(&videoInfo);
frontBuffer = new Frame(); frontBuffer = new Frame();
@@ -60,29 +59,24 @@ VideoStream::VideoStream(love::filesystem::File *file)
delete backBuffer; delete backBuffer;
delete frontBuffer; delete frontBuffer;
th_info_clear(&videoInfo); th_info_clear(&videoInfo);
ogg_sync_clear(&sync);
throw ex; throw ex;
} }
frameSync.set(new DeltaSync(), Acquire::NORETAIN); frameSync.set(new DeltaSync(), Acquire::NORETAIN);
} }
VideoStream::~VideoStream() TheoraVideoStream::~TheoraVideoStream()
{ {
if (decoder) if (decoder)
th_decode_free(decoder); th_decode_free(decoder);
th_info_clear(&videoInfo); th_info_clear(&videoInfo);
if (headerParsed)
ogg_stream_clear(&stream);
ogg_sync_clear(&sync);
delete frontBuffer; delete frontBuffer;
delete backBuffer; delete backBuffer;
} }
int VideoStream::getWidth() const int TheoraVideoStream::getWidth() const
{ {
if (headerParsed) if (headerParsed)
return videoInfo.pic_width; return videoInfo.pic_width;
@@ -90,7 +84,7 @@ int VideoStream::getWidth() const
return 0; return 0;
} }
int VideoStream::getHeight() const int TheoraVideoStream::getHeight() const
{ {
if (headerParsed) if (headerParsed)
return videoInfo.pic_height; return videoInfo.pic_height;
@@ -98,72 +92,30 @@ int VideoStream::getHeight() const
return 0; 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); love::thread::Lock l(bufferMutex);
this->frameSync = frameSync; this->frameSync = frameSync;
} }
const void *VideoStream::getFrontBuffer() const const void *TheoraVideoStream::getFrontBuffer() const
{ {
return frontBuffer; return frontBuffer;
} }
size_t VideoStream::getSize() const size_t TheoraVideoStream::getSize() const
{ {
return sizeof(Frame); return sizeof(Frame);
} }
bool VideoStream::isPlaying() const bool TheoraVideoStream::isPlaying() const
{ {
return frameSync->isPlaying() && !eos; return frameSync->isPlaying() && !demuxer.isEos();
}
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;
} }
template<typename T> template<typename T>
@@ -181,7 +133,7 @@ inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y)
} }
} }
void VideoStream::parseHeader() void TheoraVideoStream::parseHeader()
{ {
if (headerParsed) if (headerParsed)
return; return;
@@ -191,16 +143,8 @@ void VideoStream::parseHeader()
th_comment_init(&comment); th_comment_init(&comment);
int ret; int ret;
do demuxer.readPacket(packet);
{ ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
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));
if (ret < 0) if (ret < 0)
{ {
@@ -210,7 +154,7 @@ void VideoStream::parseHeader()
while (ret > 0) while (ret > 0)
{ {
readPacket(); demuxer.readPacket(packet);
ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet); ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
} }
@@ -246,111 +190,21 @@ void VideoStream::parseHeader()
th_decode_packetin(decoder, &packet, nullptr); th_decode_packetin(decoder, &packet, nullptr);
} }
// Arbitrary seeking isn't supported yet, but rewinding is void TheoraVideoStream::seekDecoder(double target)
void VideoStream::rewind()
{ {
// Seek our data stream back to the start bool success = demuxer.seek(packet, target, [this](int64 granulepos) {
file->seek(0); return th_granule_time(decoder, granulepos);
});
// Break our sync, and discard the rest of the page if (!success)
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();
return; 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 // Now update theora and our decoder on this new position of ours
lastFrame = nextFrame = -1; lastFrame = nextFrame = -1;
eos = false;
th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos)); th_decode_ctl(decoder, TH_DECCTL_SET_GRANPOS, &packet.granulepos, sizeof(packet.granulepos));
} }
void VideoStream::threadedFillBackBuffer(double dt) void TheoraVideoStream::threadedFillBackBuffer(double dt)
{ {
// Synchronize // Synchronize
frameSync->update(dt); 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 // If we're at the end of the stream, or if we're displaying the right frame
// stop here // stop here
if (eos || position < nextFrame) if (demuxer.isEos() || position < nextFrame)
return; return;
th_ycbcr_buffer bufferinfo; th_ycbcr_buffer bufferinfo;
@@ -371,7 +225,7 @@ void VideoStream::threadedFillBackBuffer(double dt)
ogg_int64_t granulePosition; ogg_int64_t granulePosition;
do do
{ {
if (readPacket()) if (demuxer.readPacket(packet))
return; return;
} while (th_decode_packetin(decoder, &packet, &granulePosition) != 0); } while (th_decode_packetin(decoder, &packet, &granulePosition) != 0);
lastFrame = nextFrame; lastFrame = nextFrame;
@@ -413,7 +267,7 @@ void VideoStream::threadedFillBackBuffer(double dt)
if (position > nextFrame) if (position > nextFrame)
{ {
if (++lagCounter > 5) if (++lagCounter > 5)
seek(position); seekDecoder(position);
} }
else else
lagCounter = 0; lagCounter = 0;
@@ -422,14 +276,14 @@ void VideoStream::threadedFillBackBuffer(double dt)
frameReady = true; frameReady = true;
} }
void VideoStream::fillBackBuffer() void TheoraVideoStream::fillBackBuffer()
{ {
// Done in worker thread // Done in worker thread
} }
bool VideoStream::swapBuffers() bool TheoraVideoStream::swapBuffers()
{ {
if (eos) if (demuxer.isEos())
return false; return false;
love::thread::Lock l(bufferMutex); love::thread::Lock l(bufferMutex);
@@ -27,6 +27,7 @@
#include "common/int.h" #include "common/int.h"
#include "filesystem/File.h" #include "filesystem/File.h"
#include "thread/threads.h" #include "thread/threads.h"
#include "OggDemuxer.h"
// OGG/Theora // OGG/Theora
#include <ogg/ogg.h> #include <ogg/ogg.h>
@@ -40,11 +41,11 @@ namespace video
namespace theora namespace theora
{ {
class VideoStream : public love::video::VideoStream class TheoraVideoStream : public love::video::VideoStream
{ {
public: public:
VideoStream(love::filesystem::File *file); TheoraVideoStream(love::filesystem::File *file);
~VideoStream(); ~TheoraVideoStream();
const void *getFrontBuffer() const; const void *getFrontBuffer() const;
size_t getSize() const; size_t getSize() const;
@@ -61,14 +62,10 @@ public:
void threadedFillBackBuffer(double dt); void threadedFillBackBuffer(double dt);
private: private:
StrongRef<love::filesystem::File> file; OggDemuxer demuxer;
bool headerParsed; bool headerParsed;
bool streamInited;
int videoSerial;
ogg_sync_state sync;
ogg_stream_state stream;
ogg_page page;
ogg_packet packet; ogg_packet packet;
th_info videoInfo; th_info videoInfo;
@@ -86,15 +83,11 @@ private:
double lastFrame; double lastFrame;
double nextFrame; double nextFrame;
bool eos;
unsigned int lagCounter; unsigned int lagCounter;
void readPage();
bool readPacket(bool mustSucceed = false); // true if eos
void parseHeader(); void parseHeader();
void rewind();
void seekDecoder(double target); void seekDecoder(double target);
}; // VideoStream }; // TheoraVideoStream
} // theora } // theora
} // video } // video
+3 -3
View File
@@ -46,7 +46,7 @@ Video::~Video()
VideoStream *Video::newVideoStream(love::filesystem::File *file) VideoStream *Video::newVideoStream(love::filesystem::File *file)
{ {
VideoStream *stream = new VideoStream(file); TheoraVideoStream *stream = new TheoraVideoStream(file);
workerThread->addStream(stream); workerThread->addStream(stream);
return stream; return stream;
} }
@@ -67,7 +67,7 @@ Worker::~Worker()
stop(); stop();
} }
void Worker::addStream(VideoStream *stream) void Worker::addStream(TheoraVideoStream *stream)
{ {
love::thread::Lock l(mutex); love::thread::Lock l(mutex);
streams.push_back(stream); streams.push_back(stream);
@@ -110,7 +110,7 @@ void Worker::threadFunction()
for (auto it = streams.begin(); it != streams.end(); ++it) for (auto it = streams.begin(); it != streams.end(); ++it)
{ {
VideoStream *stream = *it; TheoraVideoStream *stream = *it;
if (stream->getReferenceCount() == 1) if (stream->getReferenceCount() == 1)
{ {
// We're the only ones left // We're the only ones left
+4 -3
View File
@@ -28,7 +28,8 @@
#include "filesystem/File.h" #include "filesystem/File.h"
#include "video/Video.h" #include "video/Video.h"
#include "thread/threads.h" #include "thread/threads.h"
#include "VideoStream.h" #include "video/VideoStream.h"
#include "TheoraVideoStream.h"
namespace love namespace love
{ {
@@ -63,13 +64,13 @@ public:
// Implements Threadable // Implements Threadable
void threadFunction(); void threadFunction();
void addStream(VideoStream *stream); void addStream(TheoraVideoStream *stream);
// Frees itself! // Frees itself!
void stop(); void stop();
private: private:
std::vector<StrongRef<VideoStream>> streams; std::vector<StrongRef<TheoraVideoStream>> streams;
love::thread::MutexRef mutex; love::thread::MutexRef mutex;
love::thread::ConditionalRef cond; love::thread::ConditionalRef cond;