Browse Source

Add initial video playback support for Ogg Theora videos (resolves issue #66.)

The basic APIs are:

video = love.graphics.newVideo("myvideo.ogv")

love.graphics.draw(video, ...) -- Video objects are Drawables.

video:play(), video:pause()

video:getDuration(), video:tell(), video:rewind(), video:seek(seconds)

video:getSource()

video:getWidth(), video:getHeight(), video:setFilter(min, mag)

More advanced APIs include video:setSource(source), video:getStream(), and videostream:setSync.

To use a custom pixel shader when drawing a Video, call the new TexelVideo(texcoords) function instead of Texel(texture, texcoords) in order to get the pixel colors of a video frame.
Bart van Strien 9 years ago
parent
commit
22f2175bec
41 changed files with 2241 additions and 20 deletions
  1. 35 0
      CMakeLists.txt
  2. 1 0
      platform/unix/configure.ac
  3. 2 2
      platform/unix/genmodules
  4. 1 0
      src/common/Module.h
  5. 10 0
      src/common/Object.h
  6. 64 0
      src/common/Stream.h
  7. 2 0
      src/common/config.h
  8. 5 0
      src/common/types.cpp
  9. 5 0
      src/common/types.h
  10. 14 5
      src/modules/filesystem/wrap_Filesystem.cpp
  11. 2 0
      src/modules/filesystem/wrap_Filesystem.h
  12. 17 0
      src/modules/graphics/opengl/Graphics.cpp
  13. 5 0
      src/modules/graphics/opengl/Graphics.h
  14. 60 0
      src/modules/graphics/opengl/Shader.cpp
  15. 8 0
      src/modules/graphics/opengl/Shader.h
  16. 212 0
      src/modules/graphics/opengl/Video.cpp
  17. 79 0
      src/modules/graphics/opengl/Video.h
  18. 35 6
      src/modules/graphics/opengl/wrap_Graphics.cpp
  19. 1 0
      src/modules/graphics/opengl/wrap_Graphics.h
  20. 48 1
      src/modules/graphics/opengl/wrap_Graphics.lua
  21. 157 0
      src/modules/graphics/opengl/wrap_Video.cpp
  22. 38 0
      src/modules/graphics/opengl/wrap_Video.h
  23. 69 0
      src/modules/graphics/opengl/wrap_Video.lua
  24. 2 0
      src/modules/image/magpie/Image.cpp
  25. 6 0
      src/modules/love/love.cpp
  26. 1 1
      src/modules/sound/lullaby/VorbisDecoder.cpp
  27. 9 2
      src/modules/timer/Timer.cpp
  28. 1 3
      src/modules/timer/Timer.h
  29. 53 0
      src/modules/video/Video.h
  30. 168 0
      src/modules/video/VideoStream.cpp
  31. 131 0
      src/modules/video/VideoStream.h
  32. 122 0
      src/modules/video/theora/Video.cpp
  33. 81 0
      src/modules/video/theora/Video.h
  34. 402 0
      src/modules/video/theora/VideoStream.cpp
  35. 101 0
      src/modules/video/theora/VideoStream.h
  36. 86 0
      src/modules/video/wrap_Video.cpp
  37. 38 0
      src/modules/video/wrap_Video.h
  38. 131 0
      src/modules/video/wrap_VideoStream.cpp
  39. 35 0
      src/modules/video/wrap_VideoStream.h
  40. 2 0
      src/scripts/boot.lua
  41. 2 0
      src/scripts/boot.lua.h

+ 35 - 0
CMakeLists.txt

@@ -308,6 +308,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_OPENGL
 	src/modules/graphics/opengl/SpriteBatch.h
 	src/modules/graphics/opengl/Text.cpp
 	src/modules/graphics/opengl/Text.h
+	src/modules/graphics/opengl/Video.cpp
+	src/modules/graphics/opengl/Video.h
 	src/modules/graphics/opengl/wrap_Canvas.cpp
 	src/modules/graphics/opengl/wrap_Canvas.h
 	src/modules/graphics/opengl/wrap_Font.cpp
@@ -326,6 +328,8 @@ set(LOVE_SRC_MODULE_GRAPHICS_OPENGL
 	src/modules/graphics/opengl/wrap_SpriteBatch.h
 	src/modules/graphics/opengl/wrap_Text.cpp
 	src/modules/graphics/opengl/wrap_Text.h
+	src/modules/graphics/opengl/wrap_Video.cpp
+	src/modules/graphics/opengl/wrap_Video.h
 )
 
 set(LOVE_SRC_MODULE_GRAPHICS
@@ -778,6 +782,35 @@ set(LOVE_SRC_MODULE_TOUCH
 source_group("modules\\touch" FILES ${LOVE_SRC_MODULE_TOUCH_ROOT})
 source_group("modules\\touch\\sdl" FILES ${LOVE_SRC_MODULE_TOUCH_SDL})
 
+#
+# love.video
+#
+
+set(LOVE_SRC_MODULE_VIDEO_ROOT
+	src/modules/video/Video.h
+	src/modules/video/VideoStream.cpp
+	src/modules/video/VideoStream.h
+	src/modules/video/wrap_Video.cpp
+	src/modules/video/wrap_Video.h
+	src/modules/video/wrap_VideoStream.cpp
+	src/modules/video/wrap_VideoStream.h
+)
+
+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
+)
+
+set(LOVE_SRC_MODULE_VIDEO
+	${LOVE_SRC_MODULE_VIDEO_ROOT}
+	${LOVE_SRC_MODULE_VIDEO_THEORA}
+)
+
+source_group("modules\\video" FILES ${LOVE_SRC_MODULE_VIDEO_ROOT})
+source_group("modules\\video\\theora" FILES ${LOVE_SRC_MODULE_VIDEO_THEORA})
+
 #
 # love.window
 #
@@ -1203,6 +1236,7 @@ set(LOVE_LIB_SRC
 	${LOVE_SRC_MODULE_THREAD}
 	${LOVE_SRC_MODULE_TIMER}
 	${LOVE_SRC_MODULE_TOUCH}
+	${LOVE_SRC_MODULE_VIDEO}
 	${LOVE_SRC_MODULE_WINDOW}
 )
 
@@ -1222,6 +1256,7 @@ set(LOVE_MEGA_3P
 	${MEGA_LIBOGG}
 	${MEGA_LIBVORBISFILE}
 	${MEGA_LIBVORBIS}
+	${MEGA_LIBTHEORA}
 	${MEGA_LUA}
 	${MEGA_MODPLUG}
 	${MEGA_OPENAL}

+ 1 - 0
platform/unix/configure.ac

@@ -53,6 +53,7 @@ PKG_CHECK_MODULES([openal], [openal], [], [LOVE_MSG_ERROR([OpenAL])])
 PKG_CHECK_MODULES([libmodplug], [libmodplug], [], [LOVE_MSG_ERROR([libmodplug])])
 PKG_CHECK_MODULES([vorbisfile], [vorbisfile], [], [LOVE_MSG_ERROR([libvorbis and libvorbisfile])])
 PKG_CHECK_MODULES([zlib], [zlib], [], [LOVE_MSG_ERROR([zlib])])
+PKG_CHECK_MODULES([theora], [theoradec], [], [LOVE_MSG_ERROR([libtheora])])
 
 # Other libraries
 AC_SEARCH_LIBS([sqrt], [m], [], [LOVE_MSG_ERROR([the C math library])])

+ 2 - 2
platform/unix/genmodules

@@ -114,7 +114,7 @@ cat > src/Makefile.am << EOF
 AM_CPPFLAGS = -I$inc_current -I$inc_modules -I$inc_libraries -I$inc_libraries/enet/libenet/include \$(LOVE_INCLUDES) \$(FILE_OFFSET)\
 	\$(SDL_CFLAGS) \$(lua_CFLAGS) \$(freetype2_CFLAGS)\
 	\$(openal_CFLAGS) \$(zlib_CFLAGS) \$(libmodplug_CFLAGS)\
-	\$(vorbisfile_CFLAGS)
+	\$(vorbisfile_CFLAGS) \$(theora_CFLAGS)
 AUTOMAKE_OPTIONS = subdir-objects
 SUBDIRS =
 SUFFIXES = .lua .lua.h
@@ -146,7 +146,7 @@ liblove${love_amsuffix}_la_LDFLAGS = -module -export-dynamic \$(LDFLAGS)
 liblove${love_amsuffix}_la_LIBADD = \
 	\$(SDL_LIBS) \$(freetype2_LIBS) \$(lua_LIBS)\
 	\$(openal_LIBS) \$(zlib_LIBS) \$(libmodplug_LIBS)\
-	\$(vorbisfile_LIBS)
+	\$(vorbisfile_LIBS) \$(theora_LIBS)
 EOF
 
 genmodules >> src/Makefile.am

+ 1 - 0
src/common/Module.h

@@ -53,6 +53,7 @@ public:
 		M_TIMER,
 		M_TOUCH,
 		M_WINDOW,
+		M_VIDEO,
 		M_MAX_ENUM
 	};
 

+ 10 - 0
src/common/Object.h

@@ -117,6 +117,16 @@ public:
 		return object;
 	}
 
+	operator bool() const
+	{
+		return object != nullptr;
+	}
+
+	operator T*() const
+	{
+		return object;
+	}
+
 	void set(T *obj)
 	{
 		if (obj) obj->retain();

+ 64 - 0
src/common/Stream.h

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2006-2015 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_STREAM_H
+#define LOVE_STREAM_H
+
+// LOVE
+#include <stddef.h>
+#include "Object.h"
+
+namespace love
+{
+
+class Stream : public Object
+{
+public:
+	virtual ~Stream() {}
+
+	// getData and getSize are assumed to talk about
+	// the buffer
+
+	/**
+	 * A callback, gets called when some Stream consumer exhausts the data
+	 **/
+	virtual void fillBackBuffer() {}
+
+	/**
+	 * Get the front buffer, Streams are supposed to be (at least) double-buffered
+	 **/
+	virtual const void *getFrontBuffer() const = 0;
+
+	/**
+	 * Get the size of any (and in particular the front) buffer
+	 **/
+	virtual size_t getSize() const = 0;
+
+	/**
+	 * Swap buffers. Returns true if there is new data in the front buffer,
+     * false otherwise.
+	 * NOTE: If there is no back buffer ready, this call must be ignored
+	 **/
+	virtual bool swapBuffers() = 0;
+}; // Stream
+
+} // love
+
+#endif // LOVE_STREAM_H

+ 2 - 0
src/common/config.h

@@ -141,6 +141,8 @@
 #	define LOVE_ENABLE_TOUCH
 #	define LOVE_ENABLE_TOUCH_SDL
 #	define LOVE_ENABLE_UTF8
+#	define LOVE_ENABLE_VIDEO
+#	define LOVE_ENABLE_VIDEO_THEORA
 #	define LOVE_ENABLE_WINDOW
 #	define LOVE_ENABLE_WINDOW_SDL
 #	define LOVE_ENABLE_WUFF

+ 5 - 0
src/common/types.cpp

@@ -34,6 +34,7 @@ static const TypeBits *createTypeFlags()
 	b[OBJECT_ID] = one << OBJECT_ID;
 	b[DATA_ID] = (one << DATA_ID) | b[OBJECT_ID];
 	b[MODULE_ID] = (one << MODULE_ID) | b[OBJECT_ID];
+	b[STREAM_ID] = (one << STREAM_ID) | b[OBJECT_ID];
 
 	// Filesystem.
 	b[FILESYSTEM_FILE_ID] = (one << FILESYSTEM_FILE_ID) | b[OBJECT_ID];
@@ -55,6 +56,7 @@ static const TypeBits *createTypeFlags()
 	b[GRAPHICS_SHADER_ID] = (one << GRAPHICS_SHADER_ID) | b[OBJECT_ID];
 	b[GRAPHICS_MESH_ID] = (one << GRAPHICS_MESH_ID) | b[GRAPHICS_DRAWABLE_ID];
 	b[GRAPHICS_TEXT_ID] = (one << GRAPHICS_TEXT_ID) | b[GRAPHICS_DRAWABLE_ID];
+	b[GRAPHICS_VIDEO_ID] = (one << GRAPHICS_VIDEO_ID) | b[GRAPHICS_DRAWABLE_ID];
 
 	// Image.
 	b[IMAGE_IMAGE_DATA_ID] = (one << IMAGE_IMAGE_DATA_ID) | b[DATA_ID];
@@ -105,6 +107,9 @@ static const TypeBits *createTypeFlags()
 	b[THREAD_THREAD_ID] = (one << THREAD_THREAD_ID) | b[OBJECT_ID];
 	b[THREAD_CHANNEL_ID] = (one << THREAD_CHANNEL_ID) | b[OBJECT_ID];
 
+	// Video
+	b[VIDEO_VIDEO_STREAM_ID] = (one << VIDEO_VIDEO_STREAM_ID) | b[STREAM_ID];
+
 	// Modules.
 	b[MODULE_FILESYSTEM_ID] = (one << MODULE_FILESYSTEM_ID) | b[MODULE_ID];
 	b[MODULE_GRAPHICS_ID] = (one << MODULE_GRAPHICS_ID) | b[MODULE_ID];

+ 5 - 0
src/common/types.h

@@ -34,6 +34,7 @@ enum Type
 	OBJECT_ID,
 	DATA_ID,
 	MODULE_ID,
+	STREAM_ID,
 
 	// Filesystem.
 	FILESYSTEM_FILE_ID,
@@ -56,6 +57,7 @@ enum Type
 	GRAPHICS_SHADER_ID,
 	GRAPHICS_MESH_ID,
 	GRAPHICS_TEXT_ID,
+	GRAPHICS_VIDEO_ID,
 
 	// Image
 	IMAGE_IMAGE_DATA_ID,
@@ -106,6 +108,9 @@ enum Type
 	THREAD_THREAD_ID,
 	THREAD_CHANNEL_ID,
 
+	// Video
+	VIDEO_VIDEO_STREAM_ID,
+
 	// The modules themselves. Only add abstracted modules here.
 	MODULE_FILESYSTEM_ID,
 	MODULE_GRAPHICS_ID,

+ 14 - 5
src/modules/filesystem/wrap_Filesystem.cpp

@@ -155,19 +155,28 @@ int w_newFile(lua_State *L)
 	return 1;
 }
 
-FileData *luax_getfiledata(lua_State *L, int idx)
+File *luax_getfile(lua_State *L, int idx)
 {
-	FileData *data = nullptr;
 	File *file = nullptr;
-
 	if (lua_isstring(L, idx))
 	{
 		const char *filename = luaL_checkstring(L, idx);
 		file = instance()->newFile(filename);
 	}
-	else if (luax_istype(L, idx, FILESYSTEM_FILE_ID))
-	{
+	else
 		file = luax_checkfile(L, idx);
+
+	return file;
+}
+
+FileData *luax_getfiledata(lua_State *L, int idx)
+{
+	FileData *data = nullptr;
+	File *file = nullptr;
+
+	if (lua_isstring(L, idx) || luax_istype(L, idx, FILESYSTEM_FILE_ID))
+	{
+		file = luax_getfile(L, idx);
 		file->retain();
 	}
 	else if (luax_istype(L, idx, FILESYSTEM_FILE_DATA_ID))

+ 2 - 0
src/modules/filesystem/wrap_Filesystem.h

@@ -23,6 +23,7 @@
 
 // LOVE
 #include "common/runtime.h"
+#include "File.h"
 #include "FileData.h"
 
 namespace love
@@ -38,6 +39,7 @@ namespace filesystem
  * May trigger a Lua error.
  **/
 FileData *luax_getfiledata(lua_State *L, int idx);
+File *luax_getfile(lua_State *L, int idx);
 
 bool hack_setupWriteDirectory();
 int loader(lua_State *L);

+ 17 - 0
src/modules/graphics/opengl/Graphics.cpp

@@ -86,6 +86,11 @@ Graphics::~Graphics()
 		Shader::defaultShader->release();
 		Shader::defaultShader = nullptr;
 	}
+	if (Shader::defaultVideoShader)
+	{
+		Shader::defaultVideoShader->release();
+		Shader::defaultVideoShader = nullptr;
+	}
 
 	if (quadIndices)
 		delete quadIndices;
@@ -320,6 +325,13 @@ bool Graphics::setMode(int width, int height)
 		Shader::defaultShader = newShader(Shader::defaultCode[renderer]);
 	}
 
+	// and a default video shader.
+	if (!Shader::defaultVideoShader)
+	{
+		Renderer renderer = GLAD_ES_VERSION_2_0 ? RENDERER_OPENGLES : RENDERER_OPENGL;
+		Shader::defaultVideoShader = newShader(Shader::defaultVideoCode[renderer]);
+	}
+
 	// A shader should always be active, but the default shader shouldn't be
 	// returned by getShader(), so we don't do setShader(defaultShader).
 	if (!Shader::current)
@@ -819,6 +831,11 @@ Text *Graphics::newText(Font *font, const std::vector<Font::ColoredString> &text
 	return new Text(font, text);
 }
 
+Video *Graphics::newVideo(love::video::VideoStream *stream)
+{
+	return new Video(stream);
+}
+
 bool Graphics::isGammaCorrect() const
 {
 	return love::graphics::isGammaCorrect();

+ 5 - 0
src/modules/graphics/opengl/Graphics.h

@@ -37,6 +37,8 @@
 
 #include "window/Window.h"
 
+#include "video/VideoStream.h"
+
 #include "Font.h"
 #include "Image.h"
 #include "graphics/Quad.h"
@@ -47,6 +49,7 @@
 #include "Shader.h"
 #include "Mesh.h"
 #include "Text.h"
+#include "Video.h"
 
 namespace love
 {
@@ -185,6 +188,8 @@ public:
 
 	Text *newText(Font *font, const std::vector<Font::ColoredString> &text = {});
 
+	Video *newVideo(love::video::VideoStream *stream);
+
 	bool isGammaCorrect() const;
 
 	/**

+ 60 - 0
src/modules/graphics/opengl/Shader.cpp

@@ -64,8 +64,10 @@ namespace
 
 Shader *Shader::current = nullptr;
 Shader *Shader::defaultShader = nullptr;
+Shader *Shader::defaultVideoShader = nullptr;
 
 Shader::ShaderSource Shader::defaultCode[Graphics::RENDERER_MAX_ENUM];
+Shader::ShaderSource Shader::defaultVideoCode[Graphics::RENDERER_MAX_ENUM];
 
 std::vector<int> Shader::textureCounters;
 
@@ -77,6 +79,7 @@ Shader::Shader(const ShaderSource &source)
 	, lastCanvas((Canvas *) -1)
 	, lastViewport()
 	, lastPointSize(0.0f)
+	, videoTextureUnits()
 {
 	if (source.vertex.empty() && source.pixel.empty())
 		throw love::Exception("Cannot create shader: no source code!");
@@ -225,6 +228,9 @@ bool Shader::loadVolatile()
 	lastProjectionMatrix.setTranslation(nan, nan);
 	lastTransformMatrix.setTranslation(nan, nan);
 
+	for (int i = 0; i < 3; i++)
+		videoTextureUnits[i] = 0;
+
 	// zero out active texture list
 	activeTexUnits.clear();
 	activeTexUnits.insert(activeTexUnits.begin(), gl.getMaxTextureUnits() - 1, 0);
@@ -635,6 +641,57 @@ bool Shader::hasVertexAttrib(VertexAttribID attrib) const
 	return builtinAttributes[int(attrib)] != -1;
 }
 
+void Shader::setVideoTextures(GLuint ytexture, GLuint cbtexture, GLuint crtexture)
+{
+	TemporaryAttacher attacher(this);
+
+	// Set up the texture units that will be used by the shader to sample from
+	// the textures, if they haven't been set up yet.
+	if (videoTextureUnits[0] == 0)
+	{
+		const GLint locs[3] = {
+			builtinUniforms[BUILTIN_VIDEO_Y_CHANNEL],
+			builtinUniforms[BUILTIN_VIDEO_CB_CHANNEL],
+			builtinUniforms[BUILTIN_VIDEO_CR_CHANNEL]
+		};
+
+		const char *names[3] = {nullptr, nullptr, nullptr};
+		builtinNames.find(BUILTIN_VIDEO_Y_CHANNEL,  names[0]);
+		builtinNames.find(BUILTIN_VIDEO_CB_CHANNEL, names[1]);
+		builtinNames.find(BUILTIN_VIDEO_CR_CHANNEL, names[2]);
+
+		for (int i = 0; i < 3; i++)
+		{
+			if (locs[i] >= 0 && names[i] != nullptr)
+			{
+				videoTextureUnits[i] = getTextureUnit(names[i]);
+
+				// Increment global shader texture id counter for this texture
+				// unit, if we haven't already.
+				if (activeTexUnits[videoTextureUnits[i] - 1] == 0)
+					++textureCounters[videoTextureUnits[i] - 1];
+
+				glUniform1i(locs[i], videoTextureUnits[i]);
+			}
+		}
+	}
+
+	const GLuint textures[3] = {ytexture, cbtexture, crtexture};
+
+	// Bind the textures to their respective texture units.
+	for (int i = 0; i < 3; i++)
+	{
+		if (videoTextureUnits[i] != 0)
+		{
+			// Store texture id so it can be re-bound later.
+			activeTexUnits[videoTextureUnits[i] - 1] = textures[i];
+			gl.bindTextureToUnit(textures[i], videoTextureUnits[i], false);
+		}
+	}
+
+	gl.setTextureUnit(0);
+}
+
 void Shader::checkSetScreenParams()
 {
 	OpenGL::Viewport view = gl.getViewport();
@@ -898,6 +955,9 @@ StringMap<Shader::BuiltinUniform, Shader::BUILTIN_MAX_ENUM>::Entry Shader::built
 	{"NormalMatrix", Shader::BUILTIN_NORMAL_MATRIX},
 	{"love_PointSize", Shader::BUILTIN_POINT_SIZE},
 	{"love_ScreenSize", Shader::BUILTIN_SCREEN_SIZE},
+	{"love_VideoYChannel", Shader::BUILTIN_VIDEO_Y_CHANNEL},
+	{"love_VideoCbChannel", Shader::BUILTIN_VIDEO_CB_CHANNEL},
+	{"love_VideoCrChannel", Shader::BUILTIN_VIDEO_CR_CHANNEL},
 };
 
 StringMap<Shader::BuiltinUniform, Shader::BUILTIN_MAX_ENUM> Shader::builtinNames(Shader::builtinNameEntries, sizeof(Shader::builtinNameEntries));

+ 8 - 0
src/modules/graphics/opengl/Shader.h

@@ -64,6 +64,9 @@ public:
 		BUILTIN_NORMAL_MATRIX,
 		BUILTIN_POINT_SIZE,
 		BUILTIN_SCREEN_SIZE,
+		BUILTIN_VIDEO_Y_CHANNEL,
+		BUILTIN_VIDEO_CB_CHANNEL,
+		BUILTIN_VIDEO_CR_CHANNEL,
 		BUILTIN_MAX_ENUM
 	};
 
@@ -89,9 +92,11 @@ public:
 
 	// Pointer to the default Shader.
 	static Shader *defaultShader;
+	static Shader *defaultVideoShader;
 
 	// Default shader code (a shader is always required internally.)
 	static ShaderSource defaultCode[Graphics::RENDERER_MAX_ENUM];
+	static ShaderSource defaultVideoCode[Graphics::RENDERER_MAX_ENUM];
 
 	/**
 	 * Creates a new Shader using a list of source codes.
@@ -182,6 +187,7 @@ public:
 	 **/
 	bool hasVertexAttrib(VertexAttribID attrib) const;
 
+	void setVideoTextures(GLuint ytexture, GLuint cbtexture, GLuint crtexture);
 	void checkSetScreenParams();
 	void checkSetPointSize(float size);
 	void checkSetBuiltinUniforms();
@@ -263,6 +269,8 @@ private:
 	Matrix4 lastTransformMatrix;
 	Matrix4 lastProjectionMatrix;
 
+	GLuint videoTextureUnits[3];
+
 	// Counts total number of textures bound to each texture unit in all shaders
 	static std::vector<int> textureCounters;
 

+ 212 - 0
src/modules/graphics/opengl/Video.cpp

@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2006-2015 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 "Video.h"
+
+// LOVE
+#include "Shader.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+Video::Video(love::video::VideoStream *stream)
+	: stream(stream)
+	, filter(Texture::getDefaultFilter())
+{
+	filter.mipmap = Texture::FILTER_NONE;
+
+	stream->fillBackBuffer();
+
+	for (int i = 0; i < 4; i++)
+		vertices[i].r = vertices[i].g = vertices[i].b = vertices[i].a = 255;
+
+	// Vertices are ordered for use with triangle strips:
+	// 0----2
+	// |  / |
+	// | /  |
+	// 1----3
+	vertices[0].x = 0.0f;
+	vertices[0].y = 0.0f;
+	vertices[1].x = 0.0f;
+	vertices[1].y = (float) stream->getHeight();
+	vertices[2].x = (float) stream->getWidth();
+	vertices[2].y = 0.0f;
+	vertices[3].x = (float) stream->getWidth();
+	vertices[3].y = (float) stream->getHeight();
+
+	vertices[0].s = 0.0f;
+	vertices[0].t = 0.0f;
+	vertices[1].s = 0.0f;
+	vertices[1].t = 1.0f;
+	vertices[2].s = 1.0f;
+	vertices[2].t = 0.0f;
+	vertices[3].s = 1.0f;
+	vertices[3].t = 1.0f;
+
+	loadVolatile();
+}
+
+Video::~Video()
+{
+	unloadVolatile();
+}
+
+bool Video::loadVolatile()
+{
+	glGenTextures(3, &textures[0]);
+
+	// Create the textures using the initial frame data.
+	auto frame = (const love::video::VideoStream::Frame*) stream->getFrontBuffer();
+
+	gl.bindTexture(textures[0]);
+	gl.setTextureFilter(filter);
+
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->yw, frame->yh,
+	             0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->yplane);
+
+	gl.bindTexture(textures[1]);
+	gl.setTextureFilter(filter);
+
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->cw, frame->ch,
+	             0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->cbplane);
+
+	gl.bindTexture(textures[2]);
+	gl.setTextureFilter(filter);
+
+	glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame->cw, frame->ch,
+	             0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->crplane);
+
+	return true;
+}
+
+void Video::unloadVolatile()
+{
+	for (int i = 0; i < 3; i++)
+	{
+		gl.deleteTexture(textures[i]);
+		textures[i] = 0;
+	}
+}
+
+love::video::VideoStream *Video::getStream()
+{
+	return stream;
+}
+
+void Video::draw(float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky)
+{
+	update();
+
+	Shader *shader = Shader::current;
+	bool defaultShader = (shader == Shader::defaultShader);
+	if (defaultShader)
+	{
+		// If we're still using the default shader, substitute the video version
+		Shader::defaultVideoShader->attach();
+		shader = Shader::defaultVideoShader;
+	}
+
+	shader->setVideoTextures(textures[0], textures[1], textures[2]);
+
+	OpenGL::TempTransform transform(gl);
+	transform.get() *= Matrix4(x, y, angle, sx, sy, ox, oy, kx, ky);
+
+	gl.useVertexAttribArrays(ATTRIBFLAG_POS | ATTRIBFLAG_TEXCOORD);
+
+	glVertexAttribPointer(ATTRIB_POS, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), &vertices[0].x);
+	glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), &vertices[0].s);
+
+	gl.prepareDraw();
+	gl.drawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+	// If we were using the default shader, reattach it
+	if (defaultShader)
+		Shader::defaultShader->attach();
+}
+
+void Video::update()
+{
+	bool bufferschanged = stream->swapBuffers();
+	stream->fillBackBuffer();
+
+	if (bufferschanged)
+	{
+		auto frame = (const love::video::VideoStream::Frame*) stream->getFrontBuffer();
+
+		gl.bindTexture(textures[0]);
+		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->yw, frame->yh,
+		                GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->yplane);
+
+		gl.bindTexture(textures[1]);
+		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->cw, frame->ch,
+		                GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->cbplane);
+
+		gl.bindTexture(textures[2]);
+		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, frame->cw, frame->ch,
+		                GL_LUMINANCE, GL_UNSIGNED_BYTE, frame->crplane);
+	}
+}
+
+love::audio::Source *Video::getSource()
+{
+	return source;
+}
+
+void Video::setSource(love::audio::Source *source)
+{
+	this->source = source;
+}
+
+int Video::getWidth() const
+{
+	return stream->getWidth();
+}
+
+int Video::getHeight() const
+{
+	return stream->getHeight();
+}
+
+void Video::setFilter(const Texture::Filter &f)
+{
+	if (!Texture::validateFilter(f, false))
+		throw love::Exception("Invalid texture filter.");
+
+	filter = f;
+
+	for (int i = 0; i < 3; i++)
+	{
+		gl.bindTexture(textures[i]);
+		gl.setTextureFilter(filter);
+	}
+}
+
+const Texture::Filter &Video::getFilter() const
+{
+	return filter;
+}
+
+} // opengl
+} // graphics
+} // love

+ 79 - 0
src/modules/graphics/opengl/Video.h

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "common/math.h"
+#include "graphics/Drawable.h"
+#include "graphics/Volatile.h"
+#include "video/VideoStream.h"
+#include "audio/Source.h"
+
+#include "OpenGL.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+class Video : public Drawable, public Volatile
+{
+public:
+
+	Video(love::video::VideoStream *stream);
+	~Video();
+
+	// Volatile
+	bool loadVolatile();
+	void unloadVolatile();
+
+	love::video::VideoStream *getStream();
+	void draw(float x, float y, float angle, float sx, float sy, float ox, float oy, float kx, float ky);
+
+	love::audio::Source *getSource();
+	void setSource(love::audio::Source *source);
+
+	int getWidth() const;
+	int getHeight() const;
+
+	void setFilter(const Texture::Filter &f);
+	const Texture::Filter &getFilter() const;
+
+private:
+
+	void update();
+
+	StrongRef<love::video::VideoStream> stream;
+	StrongRef<love::audio::Source> source;
+
+	GLuint textures[3];
+
+	Vertex vertices[4];
+
+	Texture::Filter filter;
+
+}; // Video
+
+} // opengl
+} // graphics
+} // love

+ 35 - 6
src/modules/graphics/opengl/wrap_Graphics.cpp

@@ -25,6 +25,7 @@
 #include "image/Image.h"
 #include "font/Rasterizer.h"
 #include "filesystem/wrap_Filesystem.h"
+#include "video/VideoStream.h"
 #include "image/wrap_Image.h"
 
 #include <cassert>
@@ -862,6 +863,20 @@ int w_newText(lua_State *L)
 	return 1;
 }
 
+int w_newVideo(lua_State *L)
+{
+	if (!luax_istype(L, 1, VIDEO_VIDEO_STREAM_ID))
+		luax_convobj(L, 1, "video", "newVideoStream");
+
+	auto stream = luax_checktype<love::video::VideoStream>(L, 1, VIDEO_VIDEO_STREAM_ID);
+	Video *video = nullptr;
+
+	luax_catchexcept(L, [&]() { video = instance()->newVideo(stream); });
+	luax_pushtype(L, GRAPHICS_VIDEO_ID, video);
+	video->release();
+	return 1;
+}
+
 int w_setColor(lua_State *L)
 {
 	Colorf c;
@@ -1274,25 +1289,37 @@ int w_setDefaultShaderCode(lua_State *L)
 	lua_getfield(L, 1, "opengl");
 	lua_rawgeti(L, -1, 1);
 	lua_rawgeti(L, -2, 2);
+	lua_rawgeti(L, -3, 3);
 
 	Shader::ShaderSource openglcode;
-	openglcode.vertex = luax_checkstring(L, -2);
-	openglcode.pixel = luax_checkstring(L, -1);
+	openglcode.vertex = luax_checkstring(L, -3);
+	openglcode.pixel = luax_checkstring(L, -2);
 
-	lua_pop(L, 3);
+	Shader::ShaderSource openglVideocode;
+	openglVideocode.vertex = luax_checkstring(L, -3);
+	openglVideocode.pixel = luax_checkstring(L, -1);
+
+	lua_pop(L, 4);
 
 	lua_getfield(L, 1, "opengles");
 	lua_rawgeti(L, -1, 1);
 	lua_rawgeti(L, -2, 2);
+	lua_rawgeti(L, -3, 3);
 
 	Shader::ShaderSource openglescode;
-	openglescode.vertex = luax_checkstring(L, -2);
-	openglescode.pixel = luax_checkstring(L, -1);
+	openglescode.vertex = luax_checkstring(L, -3);
+	openglescode.pixel = luax_checkstring(L, -2);
+
+	Shader::ShaderSource openglesVideocode;
+	openglesVideocode.vertex = luax_checkstring(L, -3);
+	openglesVideocode.pixel = luax_checkstring(L, -1);
 
-	lua_pop(L, 3);
+	lua_pop(L, 4);
 
 	Shader::defaultCode[Graphics::RENDERER_OPENGL]   = openglcode;
 	Shader::defaultCode[Graphics::RENDERER_OPENGLES] = openglescode;
+	Shader::defaultVideoCode[Graphics::RENDERER_OPENGL]   = openglVideocode;
+	Shader::defaultVideoCode[Graphics::RENDERER_OPENGLES] = openglesVideocode;
 
 	return 0;
 }
@@ -1863,6 +1890,7 @@ static const luaL_Reg functions[] =
 	{ "newShader", w_newShader },
 	{ "newMesh", w_newMesh },
 	{ "newText", w_newText },
+	{ "_newVideo", w_newVideo },
 
 	{ "setColor", w_setColor },
 	{ "getColor", w_getColor },
@@ -1965,6 +1993,7 @@ static const lua_CFunction types[] =
 	luaopen_shader,
 	luaopen_mesh,
 	luaopen_text,
+	luaopen_video,
 	0
 };
 

+ 1 - 0
src/modules/graphics/opengl/wrap_Graphics.h

@@ -32,6 +32,7 @@
 #include "wrap_Shader.h"
 #include "wrap_Mesh.h"
 #include "wrap_Text.h"
+#include "wrap_Video.h"
 #include "Graphics.h"
 
 namespace love

+ 48 - 1
src/modules/graphics/opengl/wrap_Graphics.lua

@@ -165,6 +165,28 @@ varying mediump vec4 VaryingColor;
 
 uniform sampler2D _tex0_;]],
 
+	FUNCTIONS = [[
+uniform sampler2D love_VideoYChannel;
+uniform sampler2D love_VideoCbChannel;
+uniform sampler2D love_VideoCrChannel;
+
+vec4 VideoTexel(vec2 texcoords)
+{
+	vec3 yuv;
+	yuv[0] = Texel(love_VideoYChannel, texcoords).r;
+	yuv[1] = Texel(love_VideoCbChannel, texcoords).r;
+	yuv[2] = Texel(love_VideoCrChannel, texcoords).r;
+	yuv += vec3(-0.0627451017, -0.501960814, -0.501960814);
+
+	vec4 color;
+	color.r = dot(yuv, vec3(1.164,  0.000,  1.596));
+	color.g = dot(yuv, vec3(1.164, -0.391, -0.813));
+	color.b = dot(yuv, vec3(1.164,  2.018,  0.000));
+	color.a = 1.0;
+
+	return gammaCorrectColor(color);
+}]],
+
 	FOOTER = [[
 void main() {
 	// fix crashing issue in OSX when _tex0_ is unused within effect()
@@ -209,6 +231,7 @@ local function createPixelCode(pixelcode, is_multicanvas, lang)
 		love.graphics.isGammaCorrect() and "#define LOVE_GAMMA_CORRECT 1" or "",
 		GLSL.PIXEL.HEADER, GLSL.UNIFORMS,
 		GLSL.FUNCTIONS,
+		GLSL.PIXEL.FUNCTIONS,
 		lang == "glsles" and "#line 1" or "#line 0",
 		pixelcode,
 		is_multicanvas and GLSL.PIXEL.FOOTER_MULTI_CANVAS or GLSL.PIXEL.FOOTER,
@@ -309,21 +332,45 @@ vec4 position(mat4 transform_proj, vec4 vertpos) {
 	pixel = [[
 vec4 effect(mediump vec4 vcolor, Image tex, vec2 texcoord, vec2 pixcoord) {
 	return Texel(tex, texcoord) * vcolor;
-}]]
+}]],
+	videopixel = [[
+vec4 effect(mediump vec4 vcolor, Image tex, vec2 texcoord, vec2 pixcoord) {
+	return VideoTexel(texcoord) * vcolor;
+}]],
 }
 
 local defaults = {
 	opengl = {
 		createVertexCode(defaultcode.vertex, "glsl"),
 		createPixelCode(defaultcode.pixel, false, "glsl"),
+		createPixelCode(defaultcode.videopixel, false, "glsl"),
 	},
 	opengles = {
 		createVertexCode(defaultcode.vertex, "glsles"),
 		createPixelCode(defaultcode.pixel, false, "glsles"),
+		createPixelCode(defaultcode.videopixel, false, "glsles"),
 	},
 }
 
 love.graphics._setDefaultShaderCode(defaults)
 
+function love.graphics.newVideo(file, loadaudio)
+	local video = love.graphics._newVideo(file)
+	local source, success
+
+	if loadaudio ~= false then
+		success, source = pcall(love.audio.newSource, video:getStream():getFilename())
+	end
+	if success then
+		video:setSource(source)
+	elseif loadaudio == true then
+		error("Video had no audio track", 2)
+	else
+		video:getStream():setSync(love.video.newRemote())
+	end
+
+	return video
+end
+
 -- DO NOT REMOVE THE NEXT LINE. It is used to load this file as a C++ string.
 --)luastring"--"

+ 157 - 0
src/modules/graphics/opengl/wrap_Video.cpp

@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2006-2015 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 "wrap_Video.h"
+
+// Shove the wrap_Video.lua code directly into a raw string literal.
+static const char video_lua[] =
+#include "wrap_Video.lua"
+;
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+Video *luax_checkvideo(lua_State *L, int idx)
+{
+	return luax_checktype<Video>(L, idx, GRAPHICS_VIDEO_ID);
+}
+
+int w_Video_getStream(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	luax_pushtype(L, VIDEO_VIDEO_STREAM_ID, video->getStream());
+	return 1;
+}
+
+int w_Video_getSource(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	auto source = video->getSource();
+	if (source)
+		luax_pushtype(L, AUDIO_SOURCE_ID, video->getSource());
+	else
+		lua_pushnil(L);
+	return 1;
+}
+
+int w_Video_setSource(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	if (lua_isnoneornil(L, 2))
+		video->setSource(nullptr);
+	else
+	{
+		auto source = luax_checktype<love::audio::Source>(L, 2, AUDIO_SOURCE_ID);
+		video->setSource(source);
+	}
+	return 0;
+}
+
+int w_Video_getWidth(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getWidth());
+	return 1;
+}
+
+int w_Video_getHeight(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getHeight());
+	return 1;
+}
+
+int w_Video_getDimensions(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	lua_pushnumber(L, video->getWidth());
+	lua_pushnumber(L, video->getHeight());
+	return 2;
+}
+
+int w_Video_setFilter(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	Texture::Filter f = video->getFilter();
+
+	const char *minstr = luaL_checkstring(L, 2);
+	const char *magstr = luaL_optstring(L, 3, minstr);
+
+	if (!Texture::getConstant(minstr, f.min))
+		return luaL_error(L, "Invalid filter mode: %s", minstr);
+	if (!Texture::getConstant(magstr, f.mag))
+		return luaL_error(L, "Invalid filter mode: %s", magstr);
+
+	f.anisotropy = (float) luaL_optnumber(L, 4, 1.0);
+
+	luax_catchexcept(L, [&](){ video->setFilter(f); });
+	return 0;
+}
+
+int w_Video_getFilter(lua_State *L)
+{
+	Video *video = luax_checkvideo(L, 1);
+	const Texture::Filter f = video->getFilter();
+
+	const char *minstr = nullptr;
+	const char *magstr = nullptr;
+
+	if (!Texture::getConstant(f.min, minstr))
+		return luaL_error(L, "Unknown filter mode.");
+	if (!Texture::getConstant(f.mag, magstr))
+		return luaL_error(L, "Unknown filter mode.");
+
+	lua_pushstring(L, minstr);
+	lua_pushstring(L, magstr);
+	lua_pushnumber(L, f.anisotropy);
+	return 3;
+}
+
+static const luaL_Reg functions[] =
+{
+	{ "getStream", w_Video_getStream },
+	{ "getSource", w_Video_getSource },
+	{ "_setSource", w_Video_setSource },
+	{ "getWidth", w_Video_getWidth },
+	{ "getHeight", w_Video_getHeight },
+	{ "getDimensions", w_Video_getDimensions },
+	{ "setFilter", w_Video_setFilter },
+	{ "getFilter", w_Video_getFilter },
+	{ 0, 0 }
+};
+
+int luaopen_video(lua_State *L)
+{
+	int ret = luax_register_type(L, GRAPHICS_VIDEO_ID, "Video", functions, nullptr);
+
+	luaL_loadbuffer(L, video_lua, sizeof(video_lua), "Video.lua");
+	luax_gettypemetatable(L, GRAPHICS_VIDEO_ID);
+	lua_call(L, 1, 0);
+
+	return ret;
+}
+
+} // opengl
+} // graphics
+} // love

+ 38 - 0
src/modules/graphics/opengl/wrap_Video.h

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "Video.h"
+#include "common/runtime.h"
+
+namespace love
+{
+namespace graphics
+{
+namespace opengl
+{
+
+int luaopen_video(lua_State *L);
+
+} // opengl
+} // graphics
+} // love

+ 69 - 0
src/modules/graphics/opengl/wrap_Video.lua

@@ -0,0 +1,69 @@
+R"luastring"--(
+-- DO NOT REMOVE THE ABOVE LINE. It is used to load this file as a C++ string.
+-- There is a matching delimiter at the bottom of the file.
+
+--[[
+Copyright (c) 2006-2015 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.
+--]]
+
+local Video_mt = ...
+local Video = Video_mt.__index
+
+function Video:loadRemote()
+	local stream = self:getStream()
+	local remote = love.video.newRemote()
+	stream:setSync(remote)
+	return remote
+end
+
+function Video:setSource(source)
+	self:_setSource(source)
+	if source then
+		self:getStream():setSync(source)
+	else
+		self:getStream():setSync(love.video.newRemote())
+	end
+end
+
+function Video:play()
+	return self:getStream():play()
+end
+
+function Video:pause()
+	return self:getStream():pause()
+end
+
+function Video:seek(offset)
+	return self:getStream():seek(offset)
+end
+
+function Video:rewind()
+	return self:getStream():rewind()
+end
+
+function Video:tell()
+	return self:getStream():tell()
+end
+
+function Video:isPlaying()
+	return self:getStream():isPlaying()
+end
+
+-- DO NOT REMOVE THE NEXT LINE. It is used to load this file as a C++ string.
+--)luastring"--"

+ 2 - 0
src/modules/image/magpie/Image.cpp

@@ -18,6 +18,8 @@
  * 3. This notice may not be removed or altered from any source distribution.
  **/
 
+#include "common/config.h"
+
 #include "Image.h"
 
 #include "ImageData.h"

+ 6 - 0
src/modules/love/love.cpp

@@ -118,6 +118,9 @@ extern "C"
 #if defined(LOVE_ENABLE_TOUCH)
 	extern int luaopen_love_touch(lua_State*);
 #endif
+#if defined(LOVE_ENABLE_VIDEO)
+	extern int luaopen_love_video(lua_State*);
+#endif
 #if defined(LOVE_ENABLE_WINDOW)
 	extern int luaopen_love_window(lua_State*);
 #endif
@@ -174,6 +177,9 @@ static const luaL_Reg modules[] = {
 #if defined(LOVE_ENABLE_TOUCH)
 	{ "love.touch", luaopen_love_touch },
 #endif
+#if defined(LOVE_ENABLE_VIDEO)
+	{ "love.video", luaopen_love_video },
+#endif
 #if defined(LOVE_ENABLE_WINDOW)
 	{ "love.window", luaopen_love_window },
 #endif

+ 1 - 1
src/modules/sound/lullaby/VorbisDecoder.cpp

@@ -168,7 +168,7 @@ bool VorbisDecoder::accepts(const std::string &ext)
 {
 	static const std::string supported[] =
 	{
-		"ogg", "oga", ""
+		"ogg", "oga", "ogv", ""
 	};
 
 	for (int i = 0; !(supported[i].empty()); i++)

+ 9 - 2
src/modules/timer/Timer.cpp

@@ -56,7 +56,6 @@ Timer::Timer()
 	, fpsUpdateFrequency(1)
 	, frames(0)
 	, dt(0)
-	, timerPeriod(getTimerPeriod())
 {
 	prevFpsUpdate = currTime = getTime();
 }
@@ -115,8 +114,11 @@ double Timer::getTimerPeriod()
 	return 0;
 }
 
-double Timer::getTime() const
+double Timer::getTimeSinceEpoch()
 {
+	// The timer period (reciprocal of the frequency.)
+	static const double timerPeriod = getTimerPeriod();
+
 #if defined(LOVE_LINUX)
 	double mt;
 	// Check for POSIX timers and monotonic clocks. If not supported, use the gettimeofday fallback.
@@ -143,5 +145,10 @@ double Timer::getTime() const
 #endif
 }
 
+double Timer::getTime() const
+{
+	return getTimeSinceEpoch();
+}
+
 } // timer
 } // love

+ 1 - 3
src/modules/timer/Timer.h

@@ -77,6 +77,7 @@ public:
 	 * @return The time (in seconds)
 	 **/
 	virtual double getTime() const;
+	static double getTimeSinceEpoch();
 
 private:
 
@@ -98,9 +99,6 @@ private:
 	// The current timestep.
 	double dt;
 
-	// The timer period (reciprocal of the frequency.)
-	const double timerPeriod;
-
 	// Returns the timer period on some platforms.
 	static double getTimerPeriod();
 

+ 53 - 0
src/modules/video/Video.h

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2006-2015 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_VIDEO_H
+#define LOVE_VIDEO_VIDEO_H
+
+// LOVE
+#include "common/Module.h"
+#include "common/Stream.h"
+#include "filesystem/File.h"
+
+#include "VideoStream.h"
+
+namespace love
+{
+namespace video
+{
+
+class Video : public Module
+{
+public:
+	virtual ~Video() {}
+
+	// Implements Module
+	virtual ModuleType getModuleType() const { return M_VIDEO; }
+
+	/**
+	 * Create a VideoStream representing video frames
+	 **/
+	virtual VideoStream *newVideoStream(love::filesystem::File *file) = 0;
+}; // Video
+
+} // video
+} // love
+
+#endif // LOVE_VIDEO_VIDEO_H

+ 168 - 0
src/modules/video/VideoStream.cpp

@@ -0,0 +1,168 @@
+/**
+ * Copyright (c) 2006-2015 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 "VideoStream.h"
+
+using love::thread::Lock;
+
+namespace love
+{
+namespace video
+{
+
+void VideoStream::setSync(VideoStream::FrameSync *frameSync)
+{
+	this->frameSync = frameSync;
+}
+
+VideoStream::FrameSync *VideoStream::getSync() const
+{
+	return frameSync;
+}
+
+void VideoStream::play()
+{
+	frameSync->play();
+}
+
+void VideoStream::pause()
+{
+	frameSync->pause();
+}
+
+void VideoStream::seek(double offset)
+{
+	frameSync->seek(offset);
+}
+
+double VideoStream::tell()
+{
+	return frameSync->tell();
+}
+
+bool VideoStream::isPlaying()
+{
+	return frameSync->isPlaying();
+}
+
+VideoStream::Frame::Frame()
+	: yplane(nullptr)
+	, cbplane(nullptr)
+	, crplane(nullptr)
+{
+}
+
+VideoStream::Frame::~Frame()
+{
+	delete[] yplane;
+	delete[] cbplane;
+	delete[] crplane;
+}
+
+void VideoStream::FrameSync::copyState(const VideoStream::FrameSync *other)
+{
+	seek(other->tell());
+	if (other->isPlaying())
+		play();
+	else
+		pause();
+}
+
+double VideoStream::FrameSync::tell() const
+{
+	return getPosition();
+}
+
+VideoStream::DeltaSync::DeltaSync()
+	: playing(false)
+	, position(0)
+	, speed(1)
+{
+}
+
+VideoStream::DeltaSync::~DeltaSync()
+{
+}
+
+double VideoStream::DeltaSync::getPosition() const
+{
+	return position;
+}
+
+void VideoStream::DeltaSync::update(double dt)
+{
+	Lock l(mutex);
+	if (playing)
+		position += dt*speed;
+}
+
+void VideoStream::DeltaSync::play()
+{
+	playing = true;
+}
+
+void VideoStream::DeltaSync::pause()
+{
+	playing = false;
+}
+
+void VideoStream::DeltaSync::seek(double time)
+{
+	Lock l(mutex);
+	position = time;
+}
+
+bool VideoStream::DeltaSync::isPlaying() const
+{
+	return playing;
+}
+
+VideoStream::SourceSync::SourceSync(love::audio::Source *source)
+	: source(source)
+{
+}
+
+double VideoStream::SourceSync::getPosition() const
+{
+	return source->tell(love::audio::Source::UNIT_SECONDS);
+}
+
+void VideoStream::SourceSync::play()
+{
+	source->play();
+}
+
+void VideoStream::SourceSync::pause()
+{
+	source->pause();
+}
+
+void VideoStream::SourceSync::seek(double time)
+{
+	source->seek(time, love::audio::Source::UNIT_SECONDS);
+}
+
+bool VideoStream::SourceSync::isPlaying() const
+{
+	return !source->isStopped() && !source->isPaused();
+}
+
+} // video
+} // love

+ 131 - 0
src/modules/video/VideoStream.h

@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2006-2015 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_VIDEOSTREAM_H
+#define LOVE_VIDEO_VIDEOSTREAM_H
+
+// LOVE
+#include "common/Stream.h"
+#include "audio/Source.h"
+#include "thread/threads.h"
+
+namespace love
+{
+namespace video
+{
+
+class VideoStream : public Stream
+{
+public:
+	virtual ~VideoStream() {}
+
+	virtual int getWidth() const = 0;
+	virtual int getHeight() const = 0;
+	virtual const std::string &getFilename() const = 0;
+
+	// Playback api
+	virtual void play();
+	virtual void pause();
+	virtual void seek(double offset);
+	virtual double tell();
+	virtual bool isPlaying();
+
+	class FrameSync;
+	class DeltaSync;
+
+	// The stream now owns the sync, do not reuse or free
+	virtual void setSync(FrameSync *frameSync);
+	virtual FrameSync *getSync() const;
+
+	// Data structures
+	struct Frame
+	{
+		Frame();
+		~Frame();
+
+		int yw, yh;
+		unsigned char *yplane;
+
+		int cw, ch;
+		unsigned char *cbplane;
+		unsigned char *crplane;
+	};
+
+	class FrameSync : public Object
+	{
+	public:
+		virtual double getPosition() const = 0;
+		virtual void update(double /*dt*/) {}
+		virtual ~FrameSync() {}
+
+		void copyState(const FrameSync *other);
+
+		// Playback api
+		virtual void play() = 0;
+		virtual void pause() = 0;
+		virtual void seek(double offset) = 0;
+		virtual double tell() const;
+		virtual bool isPlaying() const = 0;
+	};
+
+	class DeltaSync : public FrameSync
+	{
+	public:
+		DeltaSync();
+		~DeltaSync();
+
+		virtual double getPosition() const override;
+		virtual void update(double dt) override;
+
+		virtual void play() override;
+		virtual void pause() override;
+		virtual void seek(double time) override;
+		virtual bool isPlaying() const override;
+
+	private:
+		bool playing;
+		double position;
+		double speed;
+		love::thread::MutexRef mutex;
+	};
+
+	class SourceSync : public FrameSync
+	{
+	public:
+		SourceSync(love::audio::Source *source);
+
+		virtual double getPosition() const override;
+		virtual void play() override;
+		virtual void pause() override;
+		virtual void seek(double time) override;
+		virtual bool isPlaying() const override;
+
+	private:
+		StrongRef<love::audio::Source> source;
+	};
+
+protected:
+	StrongRef<FrameSync> frameSync;
+};
+
+} // video
+} // love
+
+#endif // LOVE_VIDEO_VIDEOSTREAM_H

+ 122 - 0
src/modules/video/theora/Video.cpp

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+// STL
+#include <vector>
+
+// LOVE
+#include "Video.h"
+#include "common/delay.h"
+#include "timer/Timer.h"
+
+namespace love
+{
+namespace video
+{
+namespace theora
+{
+
+Video::Video()
+{
+	workerThread = new Worker();
+	workerThread->start();
+}
+
+Video::~Video()
+{
+	delete workerThread;
+}
+
+VideoStream *Video::newVideoStream(love::filesystem::File *file)
+{
+	VideoStream *stream = new VideoStream(file);
+	workerThread->addStream(stream);
+	return stream;
+}
+
+const char *Video::getName() const
+{
+	return "love.video.theora";
+}
+
+Worker::Worker()
+	: stopping(false)
+{
+	threadName = "VideoWorker";
+}
+
+Worker::~Worker()
+{
+	stop();
+}
+
+void Worker::addStream(VideoStream *stream)
+{
+	love::thread::Lock l(mutex);
+	streams.push_back(stream);
+}
+
+void Worker::stop()
+{
+	{
+		love::thread::Lock l(mutex);
+		stopping = true;
+	}
+
+	owner->wait();
+}
+
+void Worker::threadFunction()
+{
+	double lastFrame = love::timer::Timer::getTimeSinceEpoch();
+	while (true)
+	{
+		double curFrame = love::timer::Timer::getTimeSinceEpoch();
+		double dt = curFrame-lastFrame;
+		lastFrame = curFrame;
+
+		{
+			love::thread::Lock l(mutex);
+
+			if (stopping)
+				return;
+
+			for (auto it = streams.begin(); it != streams.end(); ++it)
+			{
+				VideoStream *stream = *it;
+				if (stream->getReferenceCount() == 1)
+				{
+					// We're the only ones left
+					streams.erase(it);
+					break;
+				}
+
+				stream->threadedFillBackBuffer(dt);
+			}
+		}
+
+		// sleep
+		love::delay(2);
+	}
+}
+
+} // theora
+} // video
+} // love

+ 81 - 0
src/modules/video/theora/Video.h

@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2006-2015 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_VIDEO_H
+#define LOVE_VIDEO_THEORA_VIDEO_H
+
+// STL
+#include <vector>
+
+// LOVE
+#include "filesystem/File.h"
+#include "video/Video.h"
+#include "thread/threads.h"
+#include "VideoStream.h"
+
+namespace love
+{
+namespace video
+{
+namespace theora
+{
+
+class Worker;
+
+class Video : public love::video::Video
+{
+public:
+	Video();
+	~Video();
+
+	// Implements Module
+	virtual const char *getName() const;
+
+	VideoStream *newVideoStream(love::filesystem::File* file);
+
+private:
+	Worker *workerThread;
+}; // Video
+
+class Worker : public love::thread::Threadable
+{
+public:
+	Worker();
+	~Worker();
+
+	// Implements Threadable
+	void threadFunction();
+
+	void addStream(VideoStream *stream);
+	// Frees itself!
+	void stop();
+
+private:
+	std::vector<StrongRef<VideoStream>> streams;
+	love::thread::MutexRef mutex;
+
+	volatile bool stopping;
+}; // Worker
+
+} // theora
+} // video
+} // love
+
+#endif // LOVE_VIDEO_THEORA_VIDEO_H

+ 402 - 0
src/modules/video/theora/VideoStream.cpp

@@ -0,0 +1,402 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+// STL
+#include <iostream>
+
+// LOVE
+#include "VideoStream.h"
+
+using love::filesystem::File;
+
+namespace love
+{
+namespace video
+{
+namespace theora
+{
+
+VideoStream::VideoStream(love::filesystem::File *file)
+	: file(file)
+	, headerParsed(false)
+	, streamInited(false)
+	, videoSerial(0)
+	, decoder(nullptr)
+	, frameReady(false)
+	, lastFrame(0)
+	, nextFrame(0)
+	, eos(false)
+	, lagCounter(0)
+{
+	ogg_sync_init(&sync);
+	th_info_init(&videoInfo);
+
+	frontBuffer = new Frame();
+	backBuffer = new Frame();
+
+	try
+	{
+		parseHeader();
+	}
+	catch (love::Exception &ex)
+	{
+		delete backBuffer;
+		delete frontBuffer;
+		th_info_clear(&videoInfo);
+		ogg_sync_clear(&sync);
+		throw ex;
+	}
+
+	frameSync = new DeltaSync();
+	frameSync->release();
+}
+
+VideoStream::~VideoStream()
+{
+	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
+{
+	if (headerParsed)
+		return videoInfo.pic_width;
+	else
+		return 0;
+}
+
+int VideoStream::getHeight() const
+{
+	if (headerParsed)
+		return videoInfo.pic_height;
+	else
+		return 0;
+}
+
+const std::string &VideoStream::getFilename() const
+{
+	return file->getFilename();
+}
+
+void VideoStream::setSync(FrameSync *frameSync)
+{
+	love::thread::Lock l(bufferMutex);
+	this->frameSync = frameSync;
+}
+
+const void *VideoStream::getFrontBuffer() const
+{
+	return frontBuffer;
+}
+
+size_t VideoStream::getSize() const
+{
+	return sizeof(Frame);
+}
+
+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)
+	{
+		// We need to read another page, but there is none, we're at the end
+		if (ogg_page_eos(&page) && !mustSucceed)
+			return eos = true;
+
+		do
+		{
+			readPage();
+		} while (ogg_page_serialno(&page) != videoSerial);
+
+		ogg_stream_pagein(&stream, &page);
+	}
+
+	return false;
+}
+
+template<typename T>
+inline void scaleFormat(th_pixel_fmt fmt, T &x, T &y)
+{
+	switch(fmt)
+	{
+	case TH_PF_420:
+		y /= 2;
+	case TH_PF_422:
+		x /= 2;
+		break;
+	default:
+		break;
+	}
+}
+
+void VideoStream::parseHeader()
+{
+	if (headerParsed)
+		return;
+
+	th_comment comment;
+	th_setup_info *setupInfo = nullptr;
+	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));
+
+	if (ret < 0)
+	{
+		th_comment_clear(&comment);
+		throw love::Exception("Could not find header");
+	}
+
+	while (ret > 0)
+	{
+		readPacket();
+		ret = th_decode_headerin(&videoInfo, &comment, &setupInfo, &packet);
+	}
+
+	th_comment_clear(&comment);
+
+	decoder = th_decode_alloc(&videoInfo, setupInfo);
+	th_setup_free(setupInfo);
+
+	Frame *buffers[2] = {backBuffer, frontBuffer};
+
+	yPlaneXOffset = cPlaneXOffset = videoInfo.pic_x;
+	yPlaneYOffset = cPlaneYOffset = videoInfo.pic_y;
+
+	scaleFormat(videoInfo.pixel_fmt, cPlaneXOffset, cPlaneYOffset);
+
+	for (int i = 0; i < 2; i++)
+	{
+		buffers[i]->cw = buffers[i]->yw = videoInfo.pic_width;
+		buffers[i]->ch = buffers[i]->yh = videoInfo.pic_height;
+
+		scaleFormat(videoInfo.pixel_fmt, buffers[i]->cw, buffers[i]->ch);
+
+		buffers[i]->yplane = new unsigned char[buffers[i]->yw * buffers[i]->yh];
+		buffers[i]->cbplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];
+		buffers[i]->crplane = new unsigned char[buffers[i]->cw * buffers[i]->ch];
+
+		memset(buffers[i]->yplane, 16, buffers[i]->yw * buffers[i]->yh);
+		memset(buffers[i]->cbplane, 128, buffers[i]->cw * buffers[i]->ch);
+		memset(buffers[i]->crplane, 128, buffers[i]->cw * buffers[i]->ch);
+	}
+
+	headerParsed = true;
+	th_decode_packetin(decoder, &packet, nullptr);
+}
+
+// Arbitrary seeking isn't supported yet, but rewinding is
+void VideoStream::rewind()
+{
+	// Seek our data stream back to the start
+	file->seek(0);
+
+	// 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)
+{
+	double low = 0;
+	double high = file->getSize();
+
+	while (high-low > 0.0001)
+	{
+		// Determine our next binary search position
+		double pos = (high-low)/2+low;
+		file->seek(pos);
+
+		// Break sync
+		ogg_sync_reset(&sync);
+		ogg_sync_pageseek(&sync, &page);
+
+		// Read a packet
+		readPacket(true);
+
+		// Determine if this is the right place
+		double curTime = th_granule_time(decoder, packet.granulepos);
+		if (curTime > target && th_granule_time(decoder, packet.granulepos-1) < target)
+			break;
+		else if (curTime > target)
+			high = pos;
+		else
+			low = 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)
+{
+	// Synchronize
+	frameSync->update(dt);
+	double position = frameSync->getPosition();
+
+	// Seeking backwards
+	if (position < lastFrame)
+	{
+		if (position < 0.01)
+			rewind();
+		else
+			seekDecoder(position);
+	}
+
+	// If we're at the end of the stream, or if we're displaying the right frame
+	// stop here
+	if (eos || position < nextFrame)
+		return;
+
+	th_ycbcr_buffer bufferinfo;
+	th_decode_ycbcr_out(decoder, bufferinfo);
+
+	ogg_int64_t granulePosition;
+	do
+	{
+		if (readPacket())
+			return;
+	} while (th_decode_packetin(decoder, &packet, &granulePosition) != 0);
+	lastFrame = nextFrame;
+	nextFrame = th_granule_time(decoder, granulePosition);
+
+	{
+		// Don't swap whilst we're writing to the backbuffer
+		love::thread::Lock l(bufferMutex);
+		frameReady = false;
+	}
+
+	for (int y = 0; y < backBuffer->yh; ++y)
+	{
+		memcpy(backBuffer->yplane+backBuffer->yw*y,
+				bufferinfo[0].data+
+					bufferinfo[0].stride*(y+yPlaneYOffset)+yPlaneXOffset,
+				backBuffer->yw);
+	}
+
+	for (int y = 0; y < backBuffer->ch; ++y)
+	{
+		memcpy(backBuffer->cbplane+backBuffer->cw*y,
+				bufferinfo[1].data+
+					bufferinfo[1].stride*(y+cPlaneYOffset)+cPlaneXOffset,
+				backBuffer->cw);
+	}
+
+	for (int y = 0; y < backBuffer->ch; ++y)
+	{
+		memcpy(backBuffer->crplane+backBuffer->cw*y,
+				bufferinfo[2].data+
+					bufferinfo[2].stride*(y+cPlaneYOffset)+cPlaneXOffset,
+				backBuffer->cw);
+	}
+
+	// Seeking forwards:
+	// If we're still not on the right frame, either we're lagging or we're seeking
+	// After 5 frames, go for a seek. This is not ideal.. but what is
+	if (position > nextFrame)
+	{
+		if (++lagCounter > 5)
+			seek(position);
+	}
+	else
+		lagCounter = 0;
+
+	love::thread::Lock l(bufferMutex);
+	frameReady = true;
+}
+
+void VideoStream::fillBackBuffer()
+{
+	// Done in worker thread
+}
+
+bool VideoStream::swapBuffers()
+{
+	if (eos)
+		return false;
+
+	love::thread::Lock l(bufferMutex);
+	if (!frameReady)
+		return false;
+	frameReady = false;
+
+	Frame *temp = frontBuffer;
+	frontBuffer = backBuffer;
+	backBuffer = temp;
+
+	return true;
+}
+
+} // theora
+} // video
+} // love

+ 101 - 0
src/modules/video/theora/VideoStream.h

@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2006-2015 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_VIDEOSTREAM_H
+#define LOVE_VIDEO_THEORA_VIDEOSTREAM_H
+
+#include "video/VideoStream.h"
+
+// LOVE
+#include "common/int.h"
+#include "filesystem/File.h"
+#include "thread/threads.h"
+
+// OGG/Theora
+#include <ogg/ogg.h>
+#include <theora/codec.h>
+#include <theora/theoradec.h>
+
+namespace love
+{
+namespace video
+{
+namespace theora
+{
+
+class VideoStream : public love::video::VideoStream
+{
+public:
+	VideoStream(love::filesystem::File *file);
+	~VideoStream();
+
+	const void *getFrontBuffer() const;
+	size_t getSize() const;
+	void fillBackBuffer();
+	bool swapBuffers();
+
+	int getWidth() const;
+	int getHeight() const;
+	const std::string &getFilename() const;
+	void setSync(FrameSync *frameSync);
+
+	void threadedFillBackBuffer(double dt);
+
+private:
+	StrongRef<love::filesystem::File> file;
+
+	bool headerParsed;
+	bool streamInited;
+	int videoSerial;
+	ogg_sync_state sync;
+	ogg_stream_state stream;
+	ogg_page page;
+	ogg_packet packet;
+
+	th_info videoInfo;
+	th_dec_ctx *decoder;
+
+	Frame *frontBuffer;
+	Frame *backBuffer;
+	unsigned int yPlaneXOffset;
+	unsigned int cPlaneXOffset;
+	unsigned int yPlaneYOffset;
+	unsigned int cPlaneYOffset;
+
+	love::thread::MutexRef bufferMutex;
+	bool frameReady;
+
+	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
+
+} // theora
+} // video
+} // love
+
+#endif // LOVE_VIDEO_THEORA_VIDEOSTREAM_H

+ 86 - 0
src/modules/video/wrap_Video.cpp

@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+// LOVE
+#include "filesystem/wrap_Filesystem.h"
+
+#include "theora/Video.h"
+#include "wrap_Video.h"
+#include "wrap_VideoStream.h"
+
+namespace love
+{
+namespace video
+{
+
+#define instance() (Module::getInstance<Video>(Module::M_VIDEO))
+
+int w_newVideoStream(lua_State *L)
+{
+	love::filesystem::File *file = love::filesystem::luax_getfile(L, 1);
+
+	VideoStream *stream = nullptr;
+	luax_catchexcept(L, [&]() {
+		// Can't check if open for reading
+		if (!file->isOpen() && !file->open(love::filesystem::File::MODE_READ))
+			luaL_error(L, "File is not open and cannot be opened");
+
+		stream = instance()->newVideoStream(file);
+	});
+
+	luax_pushtype(L, VIDEO_VIDEO_STREAM_ID, stream);
+	stream->release();
+	return 1;
+}
+
+static const lua_CFunction types[] =
+{
+	luaopen_videostream,
+	0
+};
+
+static const luaL_Reg functions[] =
+{
+	{ "newVideoStream", w_newVideoStream },
+	{ 0, 0 }
+};
+
+extern "C" int luaopen_love_video(lua_State *L)
+{
+	Video *instance = instance();
+	if (instance == nullptr)
+	{
+		luax_catchexcept(L, [&](){ instance = new love::video::theora::Video(); });
+	}
+	else
+		instance->retain();
+
+	WrappedModule w;
+	w.module = instance;
+	w.name = "video";
+	w.type = MODULE_ID;
+	w.functions = functions;
+	w.types = types;
+
+	return luax_register_module(L, w);
+}
+
+} // video
+} // love

+ 38 - 0
src/modules/video/wrap_Video.h

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2006-2015 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_WRAP_VIDEO_H
+#define LOVE_VIDEO_WRAP_VIDEO_H
+
+// LOVE
+#include "VideoStream.h"
+#include "common/runtime.h"
+
+namespace love
+{
+namespace video
+{
+
+extern "C" LOVE_EXPORT int luaopen_love_video(lua_State *L);
+
+} // video
+} // love
+
+#endif // LOVE_VIDEO_WRAP_VIDEO_H

+ 131 - 0
src/modules/video/wrap_VideoStream.cpp

@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2006-2015 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 "wrap_VideoStream.h"
+
+namespace love
+{
+namespace video
+{
+
+VideoStream *luax_checkvideostream(lua_State *L, int idx)
+{
+	return luax_checktype<VideoStream>(L, idx, VIDEO_VIDEO_STREAM_ID);
+}
+
+int w_VideoStream_setSync(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+
+	if (luax_istype(L, 2, AUDIO_SOURCE_ID))
+	{
+		auto src = luax_totype<love::audio::Source>(L, 2, AUDIO_SOURCE_ID);
+		auto sync = new VideoStream::SourceSync(src);
+		stream->setSync(sync);
+		sync->release();
+	}
+	else if (luax_istype(L, 2, VIDEO_VIDEO_STREAM_ID))
+	{
+		auto other = luax_totype<VideoStream>(L, 2, VIDEO_VIDEO_STREAM_ID);
+		stream->setSync(other->getSync());
+	}
+	else if (lua_isnoneornil(L, 2))
+	{
+		auto newSync = new VideoStream::DeltaSync();
+		newSync->copyState(stream->getSync());
+		stream->setSync(newSync);
+		newSync->release();
+	}
+	else
+		return luax_typerror(L, 2, "Source or VideoStream or nil");
+
+	return 0;
+}
+
+int w_VideoStream_getFilename(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	luax_pushstring(L, stream->getFilename());
+	return 1;
+}
+
+int w_VideoStream_play(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	stream->play();
+	return 0;
+}
+
+int w_VideoStream_pause(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	stream->pause();
+	return 0;
+}
+
+int w_VideoStream_seek(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	double offset = luaL_checknumber(L, 2);
+	stream->seek(offset);
+	return 0;
+}
+
+int w_VideoStream_rewind(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	stream->seek(0);
+	return 0;
+}
+
+int w_VideoStream_tell(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	lua_pushnumber(L, stream->tell());
+	return 1;
+}
+
+int w_VideoStream_isPlaying(lua_State *L)
+{
+	auto stream = luax_checkvideostream(L, 1);
+	luax_pushboolean(L, stream->isPlaying());
+	return 1;
+}
+
+static const luaL_Reg videostream_functions[] =
+{
+	{ "setSync", w_VideoStream_setSync },
+	{ "getFilename", w_VideoStream_getFilename },
+	{ "play", w_VideoStream_play },
+	{ "pause", w_VideoStream_pause },
+	{ "seek", w_VideoStream_seek },
+	{ "rewind", w_VideoStream_rewind },
+	{ "tell", w_VideoStream_tell },
+	{ "isPlaying", w_VideoStream_isPlaying },
+	{ 0, 0 }
+};
+
+int luaopen_videostream(lua_State *L)
+{
+	return luax_register_type(L, VIDEO_VIDEO_STREAM_ID, "VideoStream", videostream_functions, nullptr);
+}
+
+} // video
+} // love

+ 35 - 0
src/modules/video/wrap_VideoStream.h

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2006-2015 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.
+ **/
+
+#pragma once
+
+// LOVE
+#include "common/runtime.h"
+#include "VideoStream.h"
+
+namespace love
+{
+namespace video
+{
+
+LOVE_EXPORT int luaopen_videostream(lua_State *L);
+
+} // video
+} // love

+ 2 - 0
src/scripts/boot.lua

@@ -369,6 +369,7 @@ function love.init()
 			font = true,
 			thread = true,
 			window = true,
+			video = true,
 		},
 		console = false, -- Only relevant for windows.
 		identity = false,
@@ -425,6 +426,7 @@ function love.init()
 		"system",
 		"audio",
 		"image",
+		"video",
 		"font",
 		"window",
 		"graphics",

+ 2 - 0
src/scripts/boot.lua.h

@@ -675,6 +675,7 @@ const unsigned char boot_lua[] =
 	0x09, 0x09, 0x09, 0x66, 0x6f, 0x6e, 0x74, 0x20, 0x3d, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x20, 0x3d, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a,
 	0x09, 0x09, 0x09, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x20, 0x3d, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a,
+	0x09, 0x09, 0x09, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x20, 0x3d, 0x20, 0x74, 0x72, 0x75, 0x65, 0x2c, 0x0a,
 	0x09, 0x09, 0x7d, 0x2c, 0x0a,
 	0x09, 0x09, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x2c, 
 	0x20, 0x2d, 0x2d, 0x20, 0x4f, 0x6e, 0x6c, 0x79, 0x20, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x20, 
@@ -779,6 +780,7 @@ const unsigned char boot_lua[] =
 	0x09, 0x09, 0x22, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x22, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x2c, 0x0a,
+	0x09, 0x09, 0x22, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x22, 0x66, 0x6f, 0x6e, 0x74, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x22, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x22, 0x2c, 0x0a,
 	0x09, 0x09, 0x22, 0x67, 0x72, 0x61, 0x70, 0x68, 0x69, 0x63, 0x73, 0x22, 0x2c, 0x0a,