Commit bf339e85 authored by Dave Demarle's avatar Dave Demarle
Browse files

ENH: Add interface to ogg/theora video encoding to VTK and ParaView. Thanks to...

ENH: Add interface to ogg/theora video encoding to VTK and ParaView. Thanks to Michael Wild for the contribution.
parent 75e51e6d
#
# Find the native Ogg/Theora includes and libraries
#
# This module defines
# OGGTHEORA_INCLUDE_DIR, where to find ogg/ogg.h and theora/theora.h
# OGGTHEORA_LIBRARIES, the libraries to link against to use Ogg/Theora.
# OGGTHEORA_FOUND, If false, do not try to use Ogg/Theora.
FIND_PATH(OGGTHEORA_ogg_INCLUDE_DIR ogg/ogg.h)
FIND_PATH(OGGTHEORA_theora_INCLUDE_DIR theora/theora.h)
FIND_LIBRARY(OGGTHEORA_ogg_LIBRARY ogg)
FIND_LIBRARY(OGGTHEORA_theoraenc_LIBRARY theoraenc)
FIND_LIBRARY(OGGTHEORA_theoradec_LIBRARY theoradec)
SET(OGGTHEORA_INCLUDE_DIRS
${OGGTHEORA_ogg_INCLUDE_DIR}
${OGGTHEORA_theora_INCLUDE_DIR}
)
#HACK multiple directories
SET(OGGTHEORA_INCLUDE_DIR ${OGGTHEORA_INCLUDE_DIRS})
SET(OGGTHEORA_LIBRARIES
${OGGTHEORA_ogg_LIBRARY}
${OGGTHEORA_theoraenc_LIBRARY}
${OGGTHEORA_theoradec_LIBRARY}
)
#HACK multiple libraries
SET(OGGTHEORA_LIBRARY ${OGGTHEORA_LIBRARIES})
GET_FILENAME_COMPONENT(_CURRENT_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
TRY_RUN(
OGGTHEORA_NO_444_SUBSAMPLING _OGGTHEORA_COMPILE_SUCCESS
"${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/TestOggTheoraSubsampling"
"${_CURRENT_DIR}/TestOggTheoraSubsampling.c"
CMAKE_FLAGS
"-DINCLUDE_DIRECTORIES:STRING=${OGGTHEORA_INCLUDE_DIRS}"
"-DLINK_LIBRARIES:STRING=${OGGTHEORA_LIBRARIES}"
)
IF(NOT _OGGTHEORA_COMPILE_SUCCESS)
MESSAGE(SEND_ERROR "Failed to compile ${_CURRENT_DIR}/TestOggTheoraSubsampling.c")
ENDIF(NOT _OGGTHEORA_COMPILE_SUCCESS)
INCLUDE(${_CURRENT_DIR}/FindPackageHandleStandardArgs.cmake)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(OGGTHEORA
"Could NOT find the ogg and theora libraries"
OGGTHEORA_ogg_LIBRARY
OGGTHEORA_theoraenc_LIBRARY
OGGTHEORA_theoradec_LIBRARY
OGGTHEORA_ogg_INCLUDE_DIR
OGGTHEORA_theora_INCLUDE_DIR
)
MARK_AS_ADVANCED(OGGTHEORA_ogg_INCLUDE_DIR OGGTHEORA_theora_INCLUDE_DIR
OGGTHEORA_ogg_LIBRARY OGGTHEORA_theoraenc_LIBRARY
OGGTHEORA_theoradec_LIBRARY
)
/* Test to determine whether Theora supports 4:4:4 Chroma-subsampling */
#include <theora/theora.h>
#include <theora/codec.h>
#include <theora/theoraenc.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
th_info thInfo;
th_info_init(&thInfo);
thInfo.frame_width = 256;
thInfo.frame_height = 256;
thInfo.pic_width = 256;
thInfo.pic_height = 256;
thInfo.pic_x = 0;
thInfo.pic_y = 0;
thInfo.colorspace = TH_CS_ITU_REC_470BG;
thInfo.target_bitrate = 0;
thInfo.quality = 63;
thInfo.keyframe_granule_shift = 6;
thInfo.fps_numerator = 1;
thInfo.fps_denominator = 1;
thInfo.aspect_numerator = 1;
thInfo.aspect_denominator = 1;
thInfo.pixel_fmt = TH_PF_444;
th_enc_ctx *ctx = th_encode_alloc(&thInfo);
if(ctx) {
return EXIT_SUCCESS;
} else {
return EXIT_FAILURE;
}
}
......@@ -397,6 +397,11 @@ IF(VTK_USE_CARBON AND VTK_USE_COCOA)
SET(VTK_USE_COCOA 0)
ENDIF(VTK_USE_CARBON AND VTK_USE_COCOA)
#-----------------------------------------------------------------------------
# Determine whether to use the experimental Ogg/Theora writer.
OPTION(VTK_USE_OGGTHEORA_ENCODER "Build experimental Ogg/Theora support" OFF)
MARK_AS_ADVANCED(VTK_USE_OGGTHEORA_ENCODER)
#-----------------------------------------------------------------------------
# VTK requires special compiler flags on some platforms.
INCLUDE(${VTK_CMAKE_DIR}/vtkDetermineCompilerFlags.cmake)
......@@ -680,6 +685,9 @@ VTK_THIRD_PARTY_OPTION(EXPAT expat)
VTK_THIRD_PARTY_OPTION(FREETYPE freetype)
VTK_THIRD_PARTY_OPTION(LIBXML2 libxml2)
VTK_THIRD_PARTY_OPTION(LIBPROJ4 proj4)
if(VTK_USE_OGGTHEORA_ENCODER)
VTK_THIRD_PARTY_OPTION(OGGTHEORA oggtheora)
endif(VTK_USE_OGGTHEORA_ENCODER)
#-----------------------------------------------------------------------------
# Configure OpenGL support.
......
......@@ -6,6 +6,12 @@ ELSE(VTK_USE_METAIO)
SET(_VTK_METAIO_SOURCES)
ENDIF(VTK_USE_METAIO)
IF(VTK_USE_OGGTHEORA_ENCODER)
set(_VTK_OGGTHEORA_SOURCES vtkOggTheoraWriter.cxx)
ELSE(VTK_USE_OGGTHEORA_ENCODER)
set(_VTK_OGGTHEORA_SOURCES)
ENDIF(VTK_USE_OGGTHEORA_ENCODER)
SET(KIT IO)
SET(UKIT IO)
SET(KIT_TCL_LIBS vtkFilteringTCL)
......@@ -14,7 +20,7 @@ SET(KIT_JAVA_LIBS vtkFilteringJava)
SET(KIT_INTERFACE_LIBRARIES vtkFiltering)
SET(KIT_LIBS vtkDICOMParser vtkNetCDF ${_VTK_METAIO_LIB} vtksqlite
${VTK_PNG_LIBRARIES} ${VTK_ZLIB_LIBRARIES} ${VTK_JPEG_LIBRARIES}
${VTK_TIFF_LIBRARIES} ${VTK_EXPAT_LIBRARIES}
${VTK_TIFF_LIBRARIES} ${VTK_EXPAT_LIBRARIES} ${VTK_OGGTHEORA_LIBRARIES}
${KWSYS_NAMESPACE})
SET( Kit_SRCS
......@@ -83,6 +89,7 @@ vtkMoleculeReaderBase.cxx
vtkNetCDFCOARDSReader.cxx
vtkNetCDFReader.cxx
vtkOBJReader.cxx
${_VTK_OGGTHEORA_SOURCES}
vtkOutputStream.cxx
vtkOpenFOAMReader.cxx
vtkPDBReader.cxx
......
......@@ -60,6 +60,9 @@ ENDIF(VTK_SIZEOF_LONG_LONG MATCHES "^8$")
IF(VTK_HAS_UINT64_ARRAY)
SET(ConditionalTests ${ConditionalTests} TestSQLiteDatabase.cxx)
ENDIF(VTK_HAS_UINT64_ARRAY)
IF(VTK_USE_OGGTHEORA_ENCODER)
SET(ConditionalTests ${ConditionalTests} TestOggTheoraWriter.cxx)
ENDIF(VTK_USE_OGGTHEORA_ENCODER)
CREATE_TEST_SOURCELIST(Tests ${KIT}CxxTests.cxx
TestXML.cxx
......@@ -139,6 +142,9 @@ ENDIF(VTK_USE_POSTGRES AND POSTGRES_FOUND AND VTK_PSQL_TEST_URL)
IF(VTK_USE_MYSQL AND VTK_MYSQL_TEST_URL)
ADD_TEST(TestMySQLDatabase ${CXX_TEST_PATH}/${KIT}CxxTests TestMySQLDatabase)
ENDIF(VTK_USE_MYSQL AND VTK_MYSQL_TEST_URL)
IF(VTK_USE_OGGTHEORA_ENCODER)
ADD_TEST(TestOggTheoraWriter ${CXX_TEST_PATH}/${KIT}CxxTests TestOggTheoraWriter)
ENDIF(VTK_USE_OGGTHEORA_ENCODER)
IF(VTK_USE_INFOVIS)
ADD_TEST(TestDataObjectIO ${CXX_TEST_PATH}/${KIT}CxxTests TestDataObjectIO)
......
/*=========================================================================
Program: Visualization Toolkit
Module: TestOggTheoraWriter.cxx
Copyright (c) Michael Wild, Ken Martin, Will Schroeder, Bill Lorensen
All rights reserved.
See Copyright.txt or http://www.kitware.com/Copyright.htm for details.
This software is distributed WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the above copyright notice for more information.
=========================================================================*/
// .NAME TestOggTheoraWriter - Tests vtkOggTheoraWriter.
// .SECTION Description
// Creates a scene and uses OggTheoraWriter to generate a movie file. Test passes
// if the file exists and has non zero length.
#include "vtkImageCast.h"
#include "vtkImageData.h"
#include "vtkImageMandelbrotSource.h"
#include "vtkImageMapToColors.h"
#include "vtkLookupTable.h"
#include "vtkOggTheoraWriter.h"
#include "vtksys/SystemTools.hxx"
int TestOggTheoraWriter(int vtkNotUsed(argc), char* vtkNotUsed(argv)[])
{
int err = 0;
int cc = 0;
int exists = 0;
unsigned long length = 0;
vtkImageMandelbrotSource* Fractal0 = vtkImageMandelbrotSource::New();
Fractal0->SetWholeExtent( 0, 247, 0, 247, 0, 0 );
Fractal0->SetProjectionAxes( 0, 1, 2 );
Fractal0->SetOriginCX( -1.75, -1.25, 0, 0 );
Fractal0->SetSizeCX( 2.5, 2.5, 2, 1.5 );
Fractal0->SetMaximumNumberOfIterations( 100);
vtkImageCast* cast = vtkImageCast::New();
cast->SetInputConnection(Fractal0->GetOutputPort());
cast->SetOutputScalarTypeToUnsignedChar();
vtkLookupTable* table = vtkLookupTable::New();
table->SetTableRange(0, 100);
table->SetNumberOfColors(100);
table->Build();
table->SetTableValue(99, 0, 0, 0);
vtkImageMapToColors* colorize = vtkImageMapToColors::New();
colorize->SetOutputFormatToRGB();
colorize->SetLookupTable(table);
colorize->SetInputConnection(cast->GetOutputPort());
vtkOggTheoraWriter *w = vtkOggTheoraWriter::New();
w->SetInputConnection(colorize->GetOutputPort());
w->SetFileName("TestOggTheoraWriter.ogv");
cout << "Writing file TestOggTheoraWriter.ogv..." << endl;
w->Start();
for ( cc = 2; cc < 99; cc ++ )
{
cout << ".";
Fractal0->SetMaximumNumberOfIterations(cc);
table->SetTableRange(0, cc);
table->SetNumberOfColors(cc);
table->ForceBuild();
table->SetTableValue(cc-1, 0, 0, 0);
w->Write();
}
w->End();
cout << endl;
cout << "Done writing file TestOggTheoraWriter.ogv..." << endl;
w->Delete();
exists = (int) vtksys::SystemTools::FileExists("TestOggTheoraWriter.ogv");
length = vtksys::SystemTools::FileLength("TestOggTheoraWriter.ogv");
cout << "TestOggTheoraWriter.ogv file exists: " << exists << endl;
cout << "TestOggTheoraWriter.ogv file length: " << length << endl;
if (!exists)
{
err = 1;
cerr << "ERROR: 1 - Test failing because TestOggTheoraWriter.ogv file doesn't exist..." << endl;
}
else
{
vtksys::SystemTools::RemoveFile("TestOggTheoraWriter.ogv");
}
if (0==length)
{
err = 2;
cerr << "ERROR: 2 - Test failing because TestOggTheoraWriter.ogv file has zero length..." << endl;
}
colorize->Delete();
table->Delete();
cast->Delete();
Fractal0->Delete();
// err == 0 means test passes...
//
return err;
}
/*=========================================================================
Program: Visualization Toolkit
Module: vtkOggTheoraWriter.cxx
Copyright (c) Michael Wild, Ken Martin, Will Schroeder, Bill Lorensen
All rights reserved.
See Copyright.txt or http://www.kitware.com/Copyright.htm for details.
This software is distributed WITHOUT ANY WARRANTY; without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the above copyright notice for more information.
=========================================================================*/
#include "vtkOggTheoraWriter.h"
#include "vtkImageData.h"
#include "vtkObjectFactory.h"
#include "vtkErrorCode.h"
#include "vtk_oggtheora.h"
//---------------------------------------------------------------------------
class vtkOggTheoraWriterInternal
{
public:
vtkOggTheoraWriterInternal(vtkOggTheoraWriter *creator);
~vtkOggTheoraWriterInternal();
int Start();
int Write(vtkImageData *id);
void End();
int Dim[2];
int FrameRate;
private:
// Helper function to convert an RGB image into the Y'CbCr color space and
// into the data structure required by theora (i.e. 4:4:4 or 4:2:0
// subsampling will be used). Refer to http://www.theora.org/doc/Theora.pdf
// sections 4.3 and 4.3.2. Actually, the equations are inverted. However, I'm
// not sure whether VTK uses gamma-corrected RGB or not. I assume they are
// not, which is what we need here. Assume that the width and height are even
// numbers.
void RGB2YCbCr(vtkImageData *id, th_ycbcr_buffer ycbcr);
// Write the ogg/theora header information
int WriteHeader();
// Encode a single frame
int EncodeFrame(th_ycbcr_buffer ycbcr, int lastFrame);
vtkOggTheoraWriter *Writer;
size_t Off[2]; // offsets of the picture within the frame
th_enc_ctx *thEncContext; // the theora context (has to be freed)
th_ycbcr_buffer thImage; // the Y'CbCr image buffer
ogg_stream_state oggState; // the ogg stream state (has to be cleared)
FILE* outFile; // the output file stream
bool haveImageData; // indicater whether a frame has to be encoded
// (for the leap-frogging)
int openedFile;
int closedFile;
};
//---------------------------------------------------------------------------
vtkOggTheoraWriterInternal::vtkOggTheoraWriterInternal(vtkOggTheoraWriter *creator)
{
this->Writer = creator;
this->Dim[0] = 0;
this->Dim[1] = 0;
this->Off[0] = 0;
this->Off[1] = 0;
this->thEncContext = NULL;
this->outFile = NULL;
this->thImage[0].data = NULL;
this->thImage[1].data = NULL;
this->thImage[2].data = NULL;
this->openedFile = 0;
this->closedFile = 1;
this->haveImageData = false;
this->FrameRate = 25;
}
//---------------------------------------------------------------------------
vtkOggTheoraWriterInternal::~vtkOggTheoraWriterInternal()
{
if (!this->closedFile)
{
this->End();
}
}
//---------------------------------------------------------------------------
int vtkOggTheoraWriterInternal::Start()
{
this->closedFile = 0;
// ogg information
srand(time(NULL));
if (ogg_stream_init(&this->oggState,rand())!=0)
{
vtkGenericWarningMacro("Could not initialize ogg stream state.");
return 0;
}
// fill in theora information
th_info thInfo;
th_info_init(&thInfo);
// frame_width and frame_height must be multiples of 16
thInfo.frame_width = this->Dim[0]+15&~0xF;
thInfo.frame_height = this->Dim[1]+15&~0xF;
thInfo.pic_width = this->Dim[0];
thInfo.pic_height = this->Dim[1];
// force even offsets of the picture within the frame
this->Off[0] = (thInfo.frame_width-this->Dim[0])>>1&~1;
this->Off[1] = (thInfo.frame_height-this->Dim[1])>>1&~1;
thInfo.pic_x = this->Off[0];
thInfo.pic_y = this->Off[1];
thInfo.colorspace = TH_CS_ITU_REC_470BG;
if (this->Writer->GetSubsampling())
{
// 4:2:0 subsampling is the only implemented option in libtheora-1.0
thInfo.pixel_fmt = TH_PF_420;
}
else
{
thInfo.pixel_fmt = TH_PF_444;
}
thInfo.target_bitrate = 0; // variable bitrate recording (default)
// allow a variable quality/size tradeoff
//! \todo still have to find appropriate quality parameters though...
//! valid values are in [0,63].
switch (this->Writer->GetQuality())
{
case 0:
thInfo.quality = 42;
break;
case 1:
thInfo.quality = 52;
break;
default:
thInfo.quality = 63;
break;
}
thInfo.keyframe_granule_shift = 6; // default value
// the frame rate (as a fraction)
thInfo.fps_numerator = this->FrameRate;
thInfo.fps_denominator = 1;
// pixel ascpect ratio
thInfo.aspect_numerator = 1;
thInfo.aspect_denominator = 1;
// create the theora encoder context
this->thEncContext = th_encode_alloc(&thInfo);
if (!this->thEncContext)
{
vtkGenericWarningMacro(<< "Could not allocate the theora context.");
return 0;
}
// create the theora buffer (do not cheat with the frame padding,
// allocate the whole thing!)
for (size_t i=0; i<3; ++i)
{
this->thImage[i].width = thInfo.frame_width;
this->thImage[i].height = thInfo.frame_height;
if (this->Writer->GetSubsampling() && i>0)
{
// Chroma planes are subsampled by a factor of 2
this->thImage[i].width /= 2;
this->thImage[i].height /= 2;
}
// the stride is in bytes
this->thImage[i].stride = this->thImage[i].width*sizeof(unsigned char);
// make sure there's nothing left lying around...
if (this->thImage[i].data)
delete[] this->thImage[i].data;
// allocate the image plane
size_t siz = this->thImage[i].width * this->thImage[i].height;
this->thImage[i].data = new unsigned char[siz];
}
// thInfo is no longer needed
th_info_clear(&thInfo);
// Finally, open the file and start it off.
this->outFile = fopen(this->Writer->GetFileName(),"wb");
if (!this->outFile)
{
vtkGenericWarningMacro(<< "Could not open " << this->Writer->GetFileName() << "." );
return 0;
}
this->openedFile = 1;
return this->WriteHeader();
}
//---------------------------------------------------------------------------
// ripped from libtheora-1.0/examples/encoder_example.c
int vtkOggTheoraWriterInternal::WriteHeader()
{
th_comment thComment;
ogg_packet oggPacket;
ogg_page oggPage;
th_comment_init(&thComment);
// first packet will get its own page automatically
if (th_encode_flushheader(this->thEncContext,&thComment,&oggPacket)<=0)
{
vtkGenericWarningMacro("Internal Theora library error.");
return 0;
}
ogg_stream_packetin(&this->oggState,&oggPacket);
if (ogg_stream_pageout(&this->oggState,&oggPage)!=1)
{
vtkGenericWarningMacro("Internal Theora library error.");
return 0;
}
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
// remaining theora headers
int ret;
while (true)
{
ret=th_encode_flushheader(this->thEncContext,&thComment,&oggPacket);
if (ret<0)
{
vtkGenericWarningMacro("Internal Theora library error.");
return 0;
}
else if (!ret)
break;
ogg_stream_packetin(&this->oggState,&oggPacket);
}
// Flush the rest of our headers. This ensures
// the actual data in each stream will start
// on a new page, as per spec.
while (true)
{
ret = ogg_stream_flush(&this->oggState,&oggPage);
if (ret<0)
{
vtkGenericWarningMacro("Internal Theora library error.");
return 0;
}
if (ret==0)break;
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
}
th_comment_clear(&thComment);
return 1;
}
//---------------------------------------------------------------------------
int vtkOggTheoraWriterInternal::Write(vtkImageData *id)
{
// encode the frame from the last call.
// have to do leap-frogging, because otherwise we can't
// write the EOS page with the last frame in End().
int ret;
if (this->haveImageData)
{
ret = this->EncodeFrame(this->thImage,0);
this->haveImageData = false;
}
id->Update();
// convert current RGB int YCbCr color space
this->RGB2YCbCr(id,this->thImage);
this->haveImageData = true;
return ret;
}
//---------------------------------------------------------------------------
// ripped from libtheora-1.0/examples/encoder_example.c
int vtkOggTheoraWriterInternal::EncodeFrame(th_ycbcr_buffer ycbcr, int lastFrame)
{
if (th_encode_ycbcr_in(this->thEncContext,this->thImage)<0)
{
vtkGenericWarningMacro("Error encoding frame.");
return 0;
}
// retrieve and push packets, writing pages as required
ogg_packet oggPacket;
ogg_page oggPage;
int ret;
while (ret=th_encode_packetout(this->thEncContext,lastFrame,&oggPacket))
{
if(ret<0)
{
vtkGenericWarningMacro("Error retrieving packet from codec.");
return 0;
}
if (ogg_stream_packetin(&this->oggState,&oggPacket)<0)
{
vtkGenericWarningMacro("Error inserting packet into stream.");
return 0;
}
while (ogg_stream_pageout(&this->oggState,&oggPage))
{
fwrite(oggPage.header,1,oggPage.header_len,this->outFile);
fwrite(oggPage.body,1,oggPage.body_len,this->outFile);
}
}
return 1;
}
//---------------------------------------------------------------------------
void vtkOggTheoraWriterInternal::End()
{
// flush remaining frame
if (this->haveImageData)
if (!this->EncodeFrame(this->thImage,1))
vtkGenericWarningMacro("Failed to finish writing movie");
this->haveImageData = false;
// clean up
for (size_t i = 0; i < 3; ++i)
{
if (this->thImage[i].data)
{
delete[] this->thImage[i].data;
this->thImage[i].data = NULL;
}
}
if (this->thEncContext)
{
th_encode_free(this->thEncContext);
this->thEncContext = NULL;
}