2010-05-12 01:44:00 +08:00
|
|
|
/*M///////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
|
|
// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
|
|
|
|
//
|
|
|
|
// By downloading, copying, installing or using the software you agree to this license.
|
|
|
|
// If you do not agree to this license, do not download, install,
|
|
|
|
// copy or use the software.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Intel License Agreement
|
|
|
|
// For Open Source Computer Vision Library
|
|
|
|
//
|
|
|
|
// Copyright (C) 2000, Intel Corporation, all rights reserved.
|
|
|
|
// Third party copyrights are property of their respective owners.
|
|
|
|
//
|
|
|
|
// Redistribution and use in source and binary forms, with or without modification,
|
|
|
|
// are permitted provided that the following conditions are met:
|
|
|
|
//
|
|
|
|
// * Redistribution's of source code must retain the above copyright notice,
|
|
|
|
// this list of conditions and the following disclaimer.
|
|
|
|
//
|
|
|
|
// * Redistribution's in binary form must reproduce the above copyright notice,
|
|
|
|
// this list of conditions and the following disclaimer in the documentation
|
|
|
|
// and/or other materials provided with the distribution.
|
|
|
|
//
|
|
|
|
// * The name of Intel Corporation may not be used to endorse or promote products
|
|
|
|
// derived from this software without specific prior written permission.
|
|
|
|
//
|
|
|
|
// This software is provided by the copyright holders and contributors "as is" and
|
|
|
|
// any express or implied warranties, including, but not limited to, the implied
|
|
|
|
// warranties of merchantability and fitness for a particular purpose are disclaimed.
|
|
|
|
// In no event shall the Intel Corporation or contributors be liable for any direct,
|
|
|
|
// indirect, incidental, special, exemplary, or consequential damages
|
|
|
|
// (including, but not limited to, procurement of substitute goods or services;
|
|
|
|
// loss of use, data, or profits; or business interruption) however caused
|
|
|
|
// and on any theory of liability, whether in contract, strict liability,
|
|
|
|
// or tort (including negligence or otherwise) arising in any way out of
|
|
|
|
// the use of this software, even if advised of the possibility of such damage.
|
|
|
|
//
|
|
|
|
//M*/
|
|
|
|
|
|
|
|
#include "precomp.hpp"
|
|
|
|
|
|
|
|
#if defined _MSC_VER && _MSC_VER >= 1200
|
|
|
|
#pragma warning( disable: 4244 4510 4512 4610 )
|
|
|
|
#endif
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
#if !defined(WIN32) || defined(__MINGW32__)
|
|
|
|
// some versions of FFMPEG assume a C99 compiler, and don't define INT64_C
|
2010-06-11 11:59:43 +08:00
|
|
|
#if !defined INT64_C || !defined UINT64_C
|
2010-06-12 02:43:46 +08:00
|
|
|
#define INT64_C
|
|
|
|
#define UINT64_C
|
2010-05-12 01:44:00 +08:00
|
|
|
#define __STDC_CONSTANT_MACROS
|
|
|
|
// force re-inclusion of stdint.h to get INT64_C macro
|
|
|
|
#undef _STDINT_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#endif
|
|
|
|
#include <errno.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef WIN32
|
2010-08-06 01:24:41 +08:00
|
|
|
#define HAVE_FFMPEG_SWSCALE 1
|
|
|
|
#include <libavformat/avformat.h>
|
|
|
|
#include <libavcodec/avcodec.h>
|
|
|
|
#include <libswscale/swscale.h>
|
2010-05-12 01:44:00 +08:00
|
|
|
#else
|
|
|
|
|
|
|
|
// if the header path is not specified explicitly, let's deduce it
|
|
|
|
#if !defined HAVE_FFMPEG_AVCODEC_H && !defined HAVE_LIBAVCODEC_AVCODEC_H
|
|
|
|
|
|
|
|
#if defined(HAVE_GENTOO_FFMPEG)
|
|
|
|
#define HAVE_LIBAVCODEC_AVCODEC_H 1
|
|
|
|
#define HAVE_LIBAVFORMAT_AVFORMAT_H 1
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
#define HAVE_LIBSWSCALE_SWSCALE_H 1
|
|
|
|
#endif
|
|
|
|
#elif defined HAVE_FFMPEG
|
|
|
|
#define HAVE_FFMPEG_AVCODEC_H 1
|
|
|
|
#define HAVE_FFMPEG_AVFORMAT_H 1
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
#define HAVE_FFMPEG_SWSCALE_H 1
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if defined(HAVE_FFMPEG_AVCODEC_H)
|
|
|
|
#include <ffmpeg/avcodec.h>
|
|
|
|
#endif
|
|
|
|
#if defined(HAVE_FFMPEG_AVFORMAT_H)
|
|
|
|
#include <ffmpeg/avformat.h>
|
|
|
|
#endif
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE_H)
|
|
|
|
#include <ffmpeg/swscale.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if defined(HAVE_LIBAVFORMAT_AVFORMAT_H)
|
|
|
|
#include <libavformat/avformat.h>
|
|
|
|
#endif
|
|
|
|
#if defined(HAVE_LIBAVCODEC_AVCODEC_H)
|
|
|
|
#include <libavcodec/avcodec.h>
|
|
|
|
#endif
|
|
|
|
#if defined(HAVE_LIBSWSCALE_SWSCALE_H)
|
|
|
|
#include <libswscale/swscale.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#if defined _MSC_VER && _MSC_VER >= 1200
|
|
|
|
#pragma warning( default: 4244 4510 4512 4610 )
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef NDEBUG
|
|
|
|
#define CV_WARN(message)
|
|
|
|
#else
|
|
|
|
#define CV_WARN(message) fprintf(stderr, "warning: %s (%s:%d)\n", message, __FILE__, __LINE__)
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef MKTAG
|
|
|
|
#define MKTAG(a,b,c,d) (a | (b << 8) | (c << 16) | (d << 24))
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* PIX_FMT_RGBA32 macro changed in newer ffmpeg versions */
|
|
|
|
#ifndef PIX_FMT_RGBA32
|
|
|
|
#define PIX_FMT_RGBA32 PIX_FMT_RGB32
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
char * FOURCC2str( int fourcc )
|
|
|
|
{
|
|
|
|
char * mystr=(char*)malloc(5);
|
|
|
|
mystr[0]=(char)((fourcc )&255);
|
|
|
|
mystr[1]=(char)((fourcc>> 8)&255);
|
|
|
|
mystr[2]=(char)((fourcc>>16)&255);
|
|
|
|
mystr[3]=(char)((fourcc>>24)&255);
|
|
|
|
mystr[4]=0;
|
|
|
|
return mystr;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// required to look up the correct codec ID depending on the FOURCC code,
|
|
|
|
// this is just a snipped from the file riff.c from ffmpeg/libavformat
|
|
|
|
typedef struct AVCodecTag {
|
|
|
|
int id;
|
|
|
|
unsigned int tag;
|
|
|
|
} AVCodecTag;
|
|
|
|
|
|
|
|
const AVCodecTag codec_bmp_tags[] = {
|
|
|
|
{ CODEC_ID_H264, MKTAG('H', '2', '6', '4') },
|
|
|
|
{ CODEC_ID_H264, MKTAG('h', '2', '6', '4') },
|
|
|
|
{ CODEC_ID_H264, MKTAG('X', '2', '6', '4') },
|
|
|
|
{ CODEC_ID_H264, MKTAG('x', '2', '6', '4') },
|
|
|
|
{ CODEC_ID_H264, MKTAG('a', 'v', 'c', '1') },
|
|
|
|
{ CODEC_ID_H264, MKTAG('V', 'S', 'S', 'H') },
|
|
|
|
|
|
|
|
{ CODEC_ID_H263, MKTAG('H', '2', '6', '3') },
|
|
|
|
{ CODEC_ID_H263P, MKTAG('H', '2', '6', '3') },
|
|
|
|
{ CODEC_ID_H263I, MKTAG('I', '2', '6', '3') }, /* intel h263 */
|
|
|
|
{ CODEC_ID_H261, MKTAG('H', '2', '6', '1') },
|
|
|
|
|
|
|
|
/* added based on MPlayer */
|
|
|
|
{ CODEC_ID_H263P, MKTAG('U', '2', '6', '3') },
|
|
|
|
{ CODEC_ID_H263P, MKTAG('v', 'i', 'v', '1') },
|
|
|
|
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('F', 'M', 'P', '4') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('D', 'I', 'V', 'X') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('D', 'X', '5', '0') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('X', 'V', 'I', 'D') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('M', 'P', '4', 'S') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('M', '4', 'S', '2') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG(0x04, 0, 0, 0) }, /* some broken avi use this */
|
|
|
|
|
|
|
|
/* added based on MPlayer */
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('D', 'I', 'V', '1') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('B', 'L', 'Z', '0') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('m', 'p', '4', 'v') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('U', 'M', 'P', '4') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('W', 'V', '1', 'F') },
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('S', 'E', 'D', 'G') },
|
|
|
|
|
|
|
|
{ CODEC_ID_MPEG4, MKTAG('R', 'M', 'P', '4') },
|
|
|
|
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('D', 'I', 'V', '3') }, /* default signature when using MSMPEG4 */
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('M', 'P', '4', '3') },
|
|
|
|
|
|
|
|
/* added based on MPlayer */
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('M', 'P', 'G', '3') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('D', 'I', 'V', '5') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('D', 'I', 'V', '6') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('D', 'I', 'V', '4') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('A', 'P', '4', '1') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('C', 'O', 'L', '1') },
|
|
|
|
{ CODEC_ID_MSMPEG4V3, MKTAG('C', 'O', 'L', '0') },
|
|
|
|
|
|
|
|
{ CODEC_ID_MSMPEG4V2, MKTAG('M', 'P', '4', '2') },
|
|
|
|
|
|
|
|
/* added based on MPlayer */
|
|
|
|
{ CODEC_ID_MSMPEG4V2, MKTAG('D', 'I', 'V', '2') },
|
|
|
|
|
|
|
|
{ CODEC_ID_MSMPEG4V1, MKTAG('M', 'P', 'G', '4') },
|
|
|
|
|
|
|
|
{ CODEC_ID_WMV1, MKTAG('W', 'M', 'V', '1') },
|
|
|
|
|
|
|
|
/* added based on MPlayer */
|
|
|
|
{ CODEC_ID_WMV2, MKTAG('W', 'M', 'V', '2') },
|
|
|
|
{ CODEC_ID_DVVIDEO, MKTAG('d', 'v', 's', 'd') },
|
|
|
|
{ CODEC_ID_DVVIDEO, MKTAG('d', 'v', 'h', 'd') },
|
|
|
|
{ CODEC_ID_DVVIDEO, MKTAG('d', 'v', 's', 'l') },
|
|
|
|
{ CODEC_ID_DVVIDEO, MKTAG('d', 'v', '2', '5') },
|
|
|
|
{ CODEC_ID_MPEG1VIDEO, MKTAG('m', 'p', 'g', '1') },
|
|
|
|
{ CODEC_ID_MPEG1VIDEO, MKTAG('m', 'p', 'g', '2') },
|
|
|
|
{ CODEC_ID_MPEG2VIDEO, MKTAG('m', 'p', 'g', '2') },
|
|
|
|
{ CODEC_ID_MPEG2VIDEO, MKTAG('M', 'P', 'E', 'G') },
|
|
|
|
{ CODEC_ID_MPEG1VIDEO, MKTAG('P', 'I', 'M', '1') },
|
|
|
|
{ CODEC_ID_MPEG1VIDEO, MKTAG('V', 'C', 'R', '2') },
|
|
|
|
{ CODEC_ID_MPEG1VIDEO, 0x10000001 },
|
|
|
|
{ CODEC_ID_MPEG2VIDEO, 0x10000002 },
|
|
|
|
{ CODEC_ID_MPEG2VIDEO, MKTAG('D', 'V', 'R', ' ') },
|
|
|
|
{ CODEC_ID_MPEG2VIDEO, MKTAG('M', 'M', 'E', 'S') },
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('M', 'J', 'P', 'G') },
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('L', 'J', 'P', 'G') },
|
|
|
|
{ CODEC_ID_LJPEG, MKTAG('L', 'J', 'P', 'G') },
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('J', 'P', 'G', 'L') }, /* Pegasus lossless JPEG */
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('M', 'J', 'L', 'S') }, /* JPEG-LS custom FOURCC for avi - decoder */
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('j', 'p', 'e', 'g') },
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('I', 'J', 'P', 'G') },
|
|
|
|
{ CODEC_ID_MJPEG, MKTAG('A', 'V', 'R', 'n') },
|
|
|
|
{ CODEC_ID_HUFFYUV, MKTAG('H', 'F', 'Y', 'U') },
|
|
|
|
{ CODEC_ID_FFVHUFF, MKTAG('F', 'F', 'V', 'H') },
|
|
|
|
{ CODEC_ID_CYUV, MKTAG('C', 'Y', 'U', 'V') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, 0 },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('I', '4', '2', '0') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('Y', 'U', 'Y', '2') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('Y', '4', '2', '2') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('Y', 'V', '1', '2') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('U', 'Y', 'V', 'Y') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('I', 'Y', 'U', 'V') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('Y', '8', '0', '0') },
|
|
|
|
{ CODEC_ID_RAWVIDEO, MKTAG('H', 'D', 'Y', 'C') },
|
|
|
|
{ CODEC_ID_INDEO3, MKTAG('I', 'V', '3', '1') },
|
|
|
|
{ CODEC_ID_INDEO3, MKTAG('I', 'V', '3', '2') },
|
|
|
|
{ CODEC_ID_VP3, MKTAG('V', 'P', '3', '1') },
|
|
|
|
{ CODEC_ID_VP3, MKTAG('V', 'P', '3', '0') },
|
|
|
|
{ CODEC_ID_ASV1, MKTAG('A', 'S', 'V', '1') },
|
|
|
|
{ CODEC_ID_ASV2, MKTAG('A', 'S', 'V', '2') },
|
|
|
|
{ CODEC_ID_VCR1, MKTAG('V', 'C', 'R', '1') },
|
|
|
|
{ CODEC_ID_FFV1, MKTAG('F', 'F', 'V', '1') },
|
|
|
|
{ CODEC_ID_XAN_WC4, MKTAG('X', 'x', 'a', 'n') },
|
|
|
|
{ CODEC_ID_MSRLE, MKTAG('m', 'r', 'l', 'e') },
|
|
|
|
{ CODEC_ID_MSRLE, MKTAG(0x1, 0x0, 0x0, 0x0) },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('M', 'S', 'V', 'C') },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('m', 's', 'v', 'c') },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('C', 'R', 'A', 'M') },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('c', 'r', 'a', 'm') },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('W', 'H', 'A', 'M') },
|
|
|
|
{ CODEC_ID_MSVIDEO1, MKTAG('w', 'h', 'a', 'm') },
|
|
|
|
{ CODEC_ID_CINEPAK, MKTAG('c', 'v', 'i', 'd') },
|
|
|
|
{ CODEC_ID_TRUEMOTION1, MKTAG('D', 'U', 'C', 'K') },
|
|
|
|
{ CODEC_ID_MSZH, MKTAG('M', 'S', 'Z', 'H') },
|
|
|
|
{ CODEC_ID_ZLIB, MKTAG('Z', 'L', 'I', 'B') },
|
|
|
|
{ CODEC_ID_SNOW, MKTAG('S', 'N', 'O', 'W') },
|
|
|
|
{ CODEC_ID_4XM, MKTAG('4', 'X', 'M', 'V') },
|
|
|
|
{ CODEC_ID_FLV1, MKTAG('F', 'L', 'V', '1') },
|
|
|
|
{ CODEC_ID_SVQ1, MKTAG('s', 'v', 'q', '1') },
|
|
|
|
{ CODEC_ID_TSCC, MKTAG('t', 's', 'c', 'c') },
|
|
|
|
{ CODEC_ID_ULTI, MKTAG('U', 'L', 'T', 'I') },
|
|
|
|
{ CODEC_ID_VIXL, MKTAG('V', 'I', 'X', 'L') },
|
|
|
|
{ CODEC_ID_QPEG, MKTAG('Q', 'P', 'E', 'G') },
|
|
|
|
{ CODEC_ID_QPEG, MKTAG('Q', '1', '.', '0') },
|
|
|
|
{ CODEC_ID_QPEG, MKTAG('Q', '1', '.', '1') },
|
|
|
|
{ CODEC_ID_WMV3, MKTAG('W', 'M', 'V', '3') },
|
|
|
|
{ CODEC_ID_LOCO, MKTAG('L', 'O', 'C', 'O') },
|
|
|
|
{ CODEC_ID_THEORA, MKTAG('t', 'h', 'e', 'o') },
|
|
|
|
#if LIBAVCODEC_VERSION_INT>0x000409
|
|
|
|
{ CODEC_ID_WNV1, MKTAG('W', 'N', 'V', '1') },
|
|
|
|
{ CODEC_ID_AASC, MKTAG('A', 'A', 'S', 'C') },
|
|
|
|
{ CODEC_ID_INDEO2, MKTAG('R', 'T', '2', '1') },
|
|
|
|
{ CODEC_ID_FRAPS, MKTAG('F', 'P', 'S', '1') },
|
|
|
|
{ CODEC_ID_TRUEMOTION2, MKTAG('T', 'M', '2', '0') },
|
|
|
|
#endif
|
|
|
|
#if LIBAVCODEC_VERSION_INT>((50<<16)+(1<<8)+0)
|
|
|
|
{ CODEC_ID_FLASHSV, MKTAG('F', 'S', 'V', '1') },
|
|
|
|
{ CODEC_ID_JPEGLS,MKTAG('M', 'J', 'L', 'S') }, /* JPEG-LS custom FOURCC for avi - encoder */
|
|
|
|
{ CODEC_ID_VC1, MKTAG('W', 'V', 'C', '1') },
|
|
|
|
{ CODEC_ID_VC1, MKTAG('W', 'M', 'V', 'A') },
|
|
|
|
{ CODEC_ID_CSCD, MKTAG('C', 'S', 'C', 'D') },
|
|
|
|
{ CODEC_ID_ZMBV, MKTAG('Z', 'M', 'B', 'V') },
|
|
|
|
{ CODEC_ID_KMVC, MKTAG('K', 'M', 'V', 'C') },
|
|
|
|
#endif
|
|
|
|
#if LIBAVCODEC_VERSION_INT>((51<<16)+(11<<8)+0)
|
|
|
|
{ CODEC_ID_VP5, MKTAG('V', 'P', '5', '0') },
|
|
|
|
{ CODEC_ID_VP6, MKTAG('V', 'P', '6', '0') },
|
|
|
|
{ CODEC_ID_VP6, MKTAG('V', 'P', '6', '1') },
|
|
|
|
{ CODEC_ID_VP6, MKTAG('V', 'P', '6', '2') },
|
|
|
|
{ CODEC_ID_VP6F, MKTAG('V', 'P', '6', 'F') },
|
|
|
|
{ CODEC_ID_JPEG2000, MKTAG('M', 'J', '2', 'C') },
|
|
|
|
{ CODEC_ID_VMNC, MKTAG('V', 'M', 'n', 'c') },
|
|
|
|
#endif
|
|
|
|
#if LIBAVCODEC_VERSION_INT>=((51<<16)+(49<<8)+0)
|
|
|
|
// this tag seems not to exist in older versions of FFMPEG
|
|
|
|
{ CODEC_ID_TARGA, MKTAG('t', 'g', 'a', ' ') },
|
|
|
|
#endif
|
|
|
|
{ CODEC_ID_NONE, 0 },
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
class CvCapture_FFMPEG : public CvCapture
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
CvCapture_FFMPEG() { init(); }
|
|
|
|
virtual ~CvCapture_FFMPEG() { close(); }
|
|
|
|
|
|
|
|
virtual bool open( const char* filename );
|
|
|
|
virtual void close();
|
|
|
|
|
|
|
|
virtual double getProperty(int);
|
|
|
|
virtual bool setProperty(int, double);
|
|
|
|
virtual bool grabFrame();
|
|
|
|
virtual IplImage* retrieveFrame(int);
|
|
|
|
|
|
|
|
protected:
|
|
|
|
void init();
|
|
|
|
bool reopen();
|
|
|
|
bool slowSeek( int framenumber );
|
|
|
|
|
|
|
|
AVFormatContext * ic;
|
|
|
|
int video_stream;
|
|
|
|
AVStream * video_st;
|
|
|
|
AVFrame * picture;
|
|
|
|
int64_t picture_pts;
|
|
|
|
AVFrame rgb_picture;
|
|
|
|
AVPacket packet;
|
|
|
|
IplImage frame;
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
struct SwsContext *img_convert_ctx;
|
|
|
|
#endif
|
|
|
|
/*
|
|
|
|
'filename' contains the filename of the videosource,
|
|
|
|
'filename==NULL' indicates that ffmpeg's seek support works
|
|
|
|
for the particular file.
|
|
|
|
'filename!=NULL' indicates that the slow fallback function is used for seeking,
|
|
|
|
and so the filename is needed to reopen the file on backward seeking.
|
|
|
|
*/
|
|
|
|
char * filename;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
void CvCapture_FFMPEG::init()
|
|
|
|
{
|
|
|
|
ic = 0;
|
|
|
|
video_stream = -1;
|
|
|
|
video_st = 0;
|
|
|
|
picture = 0;
|
|
|
|
picture_pts = 0;
|
|
|
|
memset( &rgb_picture, 0, sizeof(rgb_picture) );
|
|
|
|
memset( &frame, 0, sizeof(frame) );
|
|
|
|
filename = 0;
|
|
|
|
packet.data = NULL;
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
img_convert_ctx = 0;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void CvCapture_FFMPEG::close()
|
|
|
|
{
|
|
|
|
if( picture )
|
|
|
|
av_free(picture);
|
|
|
|
|
|
|
|
if( video_st )
|
|
|
|
{
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
avcodec_close( video_st->codec );
|
|
|
|
#else
|
|
|
|
avcodec_close( &video_st->codec );
|
|
|
|
#endif
|
|
|
|
video_st = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( ic )
|
|
|
|
{
|
|
|
|
av_close_input_file(ic);
|
|
|
|
ic = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( rgb_picture.data[0] )
|
|
|
|
cvFree( &rgb_picture.data[0] );
|
|
|
|
|
|
|
|
// free last packet if exist
|
|
|
|
if (packet.data) {
|
|
|
|
av_free_packet (&packet);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
Used to reopen a video if the slower fallback function for seeking is used.
|
|
|
|
*/
|
|
|
|
bool CvCapture_FFMPEG::reopen()
|
|
|
|
{
|
|
|
|
if ( filename==NULL ) return false;
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
avcodec_close( video_st->codec );
|
|
|
|
#else
|
|
|
|
avcodec_close( &video_st->codec );
|
|
|
|
#endif
|
|
|
|
av_close_input_file(ic);
|
|
|
|
|
|
|
|
// reopen video
|
|
|
|
av_open_input_file(&ic, filename, NULL, 0, NULL);
|
|
|
|
av_find_stream_info(ic);
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
AVCodecContext *enc = ic->streams[video_stream]->codec;
|
|
|
|
#else
|
|
|
|
AVCodecContext *enc = &ic->streams[video_stream]->codec;
|
|
|
|
#endif
|
|
|
|
AVCodec *codec = avcodec_find_decoder(enc->codec_id);
|
|
|
|
avcodec_open(enc, codec);
|
|
|
|
video_st = ic->streams[video_stream];
|
|
|
|
|
|
|
|
// reset framenumber to zero
|
|
|
|
picture_pts=0;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bool CvCapture_FFMPEG::open( const char* _filename )
|
|
|
|
{
|
|
|
|
unsigned i;
|
|
|
|
bool valid = false;
|
|
|
|
|
|
|
|
close();
|
|
|
|
|
|
|
|
/* register all codecs, demux and protocols */
|
|
|
|
av_register_all();
|
|
|
|
|
|
|
|
#ifndef _DEBUG
|
|
|
|
// av_log_level = AV_LOG_QUIET;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
int err = av_open_input_file(&ic, _filename, NULL, 0, NULL);
|
|
|
|
if (err < 0) {
|
|
|
|
CV_WARN("Error opening file");
|
|
|
|
goto exit_func;
|
|
|
|
}
|
|
|
|
err = av_find_stream_info(ic);
|
|
|
|
if (err < 0) {
|
|
|
|
CV_WARN("Could not find codec parameters");
|
|
|
|
goto exit_func;
|
|
|
|
}
|
|
|
|
for(i = 0; i < ic->nb_streams; i++) {
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
AVCodecContext *enc = ic->streams[i]->codec;
|
|
|
|
#else
|
|
|
|
AVCodecContext *enc = &ic->streams[i]->codec;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if( CODEC_TYPE_VIDEO == enc->codec_type && video_stream < 0) {
|
|
|
|
AVCodec *codec = avcodec_find_decoder(enc->codec_id);
|
|
|
|
if (!codec ||
|
|
|
|
avcodec_open(enc, codec) < 0)
|
|
|
|
goto exit_func;
|
|
|
|
video_stream = i;
|
|
|
|
video_st = ic->streams[i];
|
|
|
|
picture = avcodec_alloc_frame();
|
|
|
|
|
|
|
|
rgb_picture.data[0] = (uint8_t*)cvAlloc(
|
|
|
|
avpicture_get_size( PIX_FMT_BGR24,
|
|
|
|
enc->width, enc->height ));
|
|
|
|
avpicture_fill( (AVPicture*)&rgb_picture, rgb_picture.data[0],
|
|
|
|
PIX_FMT_BGR24, enc->width, enc->height );
|
|
|
|
|
|
|
|
cvInitImageHeader( &frame, cvSize( enc->width,
|
|
|
|
enc->height ), 8, 3, 0, 4 );
|
|
|
|
cvSetData( &frame, rgb_picture.data[0],
|
|
|
|
rgb_picture.linesize[0] );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(video_stream >= 0) valid = true;
|
|
|
|
|
|
|
|
// perform check if source is seekable via ffmpeg's seek function av_seek_frame(...)
|
|
|
|
err = av_seek_frame(ic, video_stream, 10, 0);
|
|
|
|
if (err < 0)
|
|
|
|
{
|
|
|
|
filename=(char*)malloc(strlen(_filename)+1);
|
|
|
|
strcpy(filename, _filename);
|
|
|
|
// reopen videofile to 'seek' back to first frame
|
|
|
|
reopen();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// seek seems to work, so we don't need the filename,
|
|
|
|
// but we still need to seek back to filestart
|
|
|
|
filename=NULL;
|
|
|
|
av_seek_frame(ic, video_stream, 0, 0);
|
|
|
|
}
|
|
|
|
exit_func:
|
|
|
|
|
|
|
|
if( !valid )
|
|
|
|
close();
|
|
|
|
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CvCapture_FFMPEG::grabFrame()
|
|
|
|
{
|
|
|
|
bool valid = false;
|
|
|
|
static bool bFirstTime = true;
|
|
|
|
int got_picture;
|
|
|
|
|
|
|
|
// First time we're called, set packet.data to NULL to indicate it
|
|
|
|
// doesn't have to be freed
|
|
|
|
if (bFirstTime) {
|
|
|
|
bFirstTime = false;
|
|
|
|
packet.data = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !ic || !video_st )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// free last packet if exist
|
|
|
|
if (packet.data != NULL) {
|
|
|
|
av_free_packet (&packet);
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the next frame
|
2010-11-22 21:07:43 +08:00
|
|
|
while (!valid) {
|
|
|
|
int ret = av_read_frame(ic, &packet);
|
|
|
|
if (ret == AVERROR(EAGAIN))
|
|
|
|
continue;
|
|
|
|
if (ret < 0)
|
|
|
|
break;
|
|
|
|
|
2010-05-12 01:44:00 +08:00
|
|
|
if( packet.stream_index != video_stream ) {
|
|
|
|
av_free_packet (&packet);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
avcodec_decode_video(video_st->codec,
|
|
|
|
picture, &got_picture,
|
|
|
|
packet.data, packet.size);
|
|
|
|
#else
|
|
|
|
avcodec_decode_video(&video_st->codec,
|
|
|
|
picture, &got_picture,
|
|
|
|
packet.data, packet.size);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if (got_picture) {
|
|
|
|
// we have a new picture, so memorize it
|
|
|
|
picture_pts = packet.pts;
|
|
|
|
valid = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return if we have a new picture or not
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IplImage* CvCapture_FFMPEG::retrieveFrame(int)
|
|
|
|
{
|
|
|
|
if( !video_st || !picture->data[0] )
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
#if !defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
img_convert( (AVPicture*)&rgb_picture, PIX_FMT_BGR24,
|
|
|
|
(AVPicture*)picture,
|
|
|
|
video_st->codec->pix_fmt,
|
|
|
|
video_st->codec->width,
|
|
|
|
video_st->codec->height );
|
|
|
|
#else
|
|
|
|
img_convert( (AVPicture*)&rgb_picture, PIX_FMT_BGR24,
|
|
|
|
(AVPicture*)picture,
|
|
|
|
video_st->codec.pix_fmt,
|
|
|
|
video_st->codec.width,
|
|
|
|
video_st->codec.height );
|
|
|
|
#endif
|
|
|
|
#else
|
|
|
|
img_convert_ctx = sws_getContext(video_st->codec->width,
|
|
|
|
video_st->codec->height,
|
|
|
|
video_st->codec->pix_fmt,
|
|
|
|
video_st->codec->width,
|
|
|
|
video_st->codec->height,
|
|
|
|
PIX_FMT_BGR24,
|
|
|
|
SWS_BICUBIC,
|
|
|
|
NULL, NULL, NULL);
|
|
|
|
|
|
|
|
sws_scale(img_convert_ctx, picture->data,
|
|
|
|
picture->linesize, 0,
|
|
|
|
video_st->codec->height,
|
|
|
|
rgb_picture.data, rgb_picture.linesize);
|
|
|
|
sws_freeContext(img_convert_ctx);
|
|
|
|
#endif
|
|
|
|
return &frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if defined(__APPLE__)
|
|
|
|
#define AV_NOPTS_VALUE_ ((int64_t)0x8000000000000000LL)
|
|
|
|
#else
|
|
|
|
#define AV_NOPTS_VALUE_ ((int64_t)AV_NOPTS_VALUE)
|
|
|
|
#endif
|
|
|
|
|
|
|
|
double CvCapture_FFMPEG::getProperty( int property_id )
|
|
|
|
{
|
|
|
|
// if( !capture || !video_st || !picture->data[0] ) return 0;
|
|
|
|
if( !video_st ) return 0;
|
|
|
|
|
|
|
|
|
|
|
|
int64_t timestamp;
|
|
|
|
timestamp = picture_pts;
|
|
|
|
|
|
|
|
switch( property_id )
|
|
|
|
{
|
|
|
|
case CV_CAP_PROP_POS_MSEC:
|
|
|
|
if(video_st->parser && video_st->parser->dts != AV_NOPTS_VALUE_)
|
|
|
|
return (((double)video_st->parser->dts-1) *1000.0f) * av_q2d (video_st->time_base);
|
|
|
|
if(video_st->cur_dts != AV_NOPTS_VALUE_)
|
|
|
|
return (((double)video_st->cur_dts-1) *1000.0f * av_q2d (video_st->time_base));
|
|
|
|
// return (((double)video_st->cur_dts-1) *1000) / av_q2d (video_st->r_frame_rate);
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_POS_FRAMES:
|
|
|
|
if(video_st->parser && video_st->parser->dts != AV_NOPTS_VALUE_)
|
|
|
|
return (double)video_st->parser->dts-1;
|
|
|
|
if(video_st->cur_dts != AV_NOPTS_VALUE_)
|
|
|
|
return (double)video_st->cur_dts-1;
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_POS_AVI_RATIO:
|
|
|
|
if(video_st->parser && video_st->parser->dts != AV_NOPTS_VALUE_)
|
|
|
|
return (double)(video_st->parser->dts-1)/(double)video_st->duration;
|
|
|
|
if(video_st->cur_dts != AV_NOPTS_VALUE_ && video_st->duration != AV_NOPTS_VALUE_)
|
|
|
|
return (double)(video_st->cur_dts-1)/(double)video_st->duration;
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_FRAME_COUNT:
|
|
|
|
if(video_st->duration != AV_NOPTS_VALUE_)
|
|
|
|
return (double)video_st->duration;
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_FRAME_WIDTH:
|
|
|
|
return (double)frame.width;
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_FRAME_HEIGHT:
|
|
|
|
return (double)frame.height;
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_FPS:
|
|
|
|
#if LIBAVCODEC_BUILD > 4753
|
|
|
|
return av_q2d (video_st->r_frame_rate);
|
|
|
|
#else
|
|
|
|
return (double)video_st->codec.frame_rate
|
|
|
|
/ (double)video_st->codec.frame_rate_base;
|
|
|
|
#endif
|
|
|
|
break;
|
|
|
|
case CV_CAP_PROP_FOURCC:
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
return (double)video_st->codec->codec_tag;
|
|
|
|
#else
|
|
|
|
return (double)video_st->codec.codec_tag;
|
|
|
|
#endif
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// this is a VERY slow fallback function, ONLY used if ffmpeg's av_seek_frame delivers no correct result!
|
|
|
|
bool CvCapture_FFMPEG::slowSeek( int framenumber )
|
|
|
|
{
|
|
|
|
if ( framenumber>picture_pts )
|
|
|
|
{
|
|
|
|
while ( picture_pts<framenumber )
|
|
|
|
if ( !grabFrame() ) return false;
|
|
|
|
}
|
|
|
|
else if ( framenumber<picture_pts )
|
|
|
|
{
|
|
|
|
reopen();
|
|
|
|
while ( picture_pts<framenumber )
|
|
|
|
if ( !grabFrame() ) return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CvCapture_FFMPEG::setProperty( int property_id, double value )
|
|
|
|
{
|
|
|
|
if( !video_st ) return false;
|
|
|
|
|
|
|
|
switch( property_id )
|
|
|
|
{
|
|
|
|
case CV_CAP_PROP_POS_MSEC:
|
|
|
|
case CV_CAP_PROP_POS_FRAMES:
|
|
|
|
case CV_CAP_PROP_POS_AVI_RATIO:
|
|
|
|
{
|
|
|
|
int64_t timestamp = 0;
|
|
|
|
AVRational time_base;
|
|
|
|
switch( property_id )
|
|
|
|
{
|
|
|
|
case CV_CAP_PROP_POS_FRAMES:
|
|
|
|
timestamp=(int64_t)value;
|
|
|
|
if(ic->start_time != AV_NOPTS_VALUE_)
|
|
|
|
timestamp += ic->start_time;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CV_CAP_PROP_POS_MSEC:
|
|
|
|
time_base=ic->streams[video_stream]->time_base;
|
|
|
|
timestamp=(int64_t)(value*(float(time_base.den)/float(time_base.num))/1000);
|
|
|
|
if(ic->start_time != AV_NOPTS_VALUE_)
|
|
|
|
timestamp += ic->start_time;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CV_CAP_PROP_POS_AVI_RATIO:
|
|
|
|
timestamp=(int64_t)(value*ic->duration);
|
|
|
|
if(ic->start_time != AV_NOPTS_VALUE_ && ic->duration != AV_NOPTS_VALUE_)
|
|
|
|
timestamp += ic->start_time;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( filename )
|
|
|
|
{
|
|
|
|
// ffmpeg's seek doesn't work...
|
|
|
|
if (!slowSeek((int)timestamp))
|
|
|
|
{
|
|
|
|
fprintf(stderr, "HIGHGUI ERROR: AVI: could not (slow) seek to position %0.3f\n",
|
|
|
|
(double)timestamp / AV_TIME_BASE);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
int ret = av_seek_frame(ic, video_stream, timestamp, 0);
|
|
|
|
if (ret < 0)
|
|
|
|
{
|
|
|
|
fprintf(stderr, "HIGHGUI ERROR: AVI: could not seek to position %0.3f\n",
|
|
|
|
(double)timestamp / AV_TIME_BASE);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
picture_pts=(int64_t)value;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CvCapture* cvCreateFileCapture_FFMPEG( const char* filename )
|
|
|
|
{
|
|
|
|
CvCapture_FFMPEG* capture = new CvCapture_FFMPEG;
|
|
|
|
if( capture->open( filename ))
|
|
|
|
return capture;
|
|
|
|
delete capture;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
///////////////// FFMPEG CvVideoWriter implementation //////////////////////////
|
|
|
|
class CvVideoWriter_FFMPEG : public CvVideoWriter
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
CvVideoWriter_FFMPEG() { init(); }
|
|
|
|
virtual ~CvVideoWriter_FFMPEG() { close(); }
|
|
|
|
|
|
|
|
virtual bool open( const char* filename, int fourcc,
|
|
|
|
double fps, CvSize frameSize, bool isColor );
|
|
|
|
virtual void close();
|
|
|
|
virtual bool writeFrame( const IplImage* image );
|
|
|
|
|
|
|
|
protected:
|
|
|
|
void init();
|
|
|
|
|
|
|
|
AVOutputFormat *fmt;
|
|
|
|
AVFormatContext *oc;
|
|
|
|
uint8_t * outbuf;
|
|
|
|
uint32_t outbuf_size;
|
|
|
|
FILE * outfile;
|
|
|
|
AVFrame * picture;
|
|
|
|
AVFrame * input_picture;
|
|
|
|
uint8_t * picbuf;
|
|
|
|
AVStream * video_st;
|
|
|
|
int input_pix_fmt;
|
|
|
|
IplImage * temp_image;
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
struct SwsContext *img_convert_ctx;
|
|
|
|
#endif
|
|
|
|
};
|
|
|
|
|
|
|
|
static const char * icvFFMPEGErrStr(int err)
|
|
|
|
{
|
|
|
|
switch(err) {
|
|
|
|
case AVERROR_NUMEXPECTED:
|
|
|
|
return "Incorrect filename syntax";
|
|
|
|
case AVERROR_INVALIDDATA:
|
|
|
|
return "Invalid data in header";
|
|
|
|
case AVERROR_NOFMT:
|
|
|
|
return "Unknown format";
|
|
|
|
case AVERROR_IO:
|
|
|
|
return "I/O error occurred";
|
|
|
|
case AVERROR_NOMEM:
|
|
|
|
return "Memory allocation error";
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return "Unspecified error";
|
|
|
|
}
|
|
|
|
|
|
|
|
/* function internal to FFMPEG (libavformat/riff.c) to lookup codec id by fourcc tag*/
|
|
|
|
extern "C" {
|
|
|
|
enum CodecID codec_get_bmp_id(unsigned int tag);
|
|
|
|
}
|
|
|
|
|
|
|
|
void CvVideoWriter_FFMPEG::init()
|
|
|
|
{
|
|
|
|
fmt = 0;
|
|
|
|
oc = 0;
|
|
|
|
outbuf = 0;
|
|
|
|
outbuf_size = 0;
|
|
|
|
outfile = 0;
|
|
|
|
picture = 0;
|
|
|
|
input_picture = 0;
|
|
|
|
picbuf = 0;
|
|
|
|
video_st = 0;
|
|
|
|
input_pix_fmt = 0;
|
|
|
|
temp_image = 0;
|
|
|
|
#if defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
img_convert_ctx = 0;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* the following function is a modified version of code
|
|
|
|
* found in ffmpeg-0.4.9-pre1/output_example.c
|
|
|
|
*/
|
|
|
|
static AVFrame * icv_alloc_picture_FFMPEG(int pix_fmt, int width, int height, bool alloc)
|
|
|
|
{
|
|
|
|
AVFrame * picture;
|
|
|
|
uint8_t * picture_buf;
|
|
|
|
int size;
|
|
|
|
|
|
|
|
picture = avcodec_alloc_frame();
|
|
|
|
if (!picture)
|
|
|
|
return NULL;
|
|
|
|
size = avpicture_get_size( (PixelFormat) pix_fmt, width, height);
|
|
|
|
if(alloc){
|
|
|
|
picture_buf = (uint8_t *) cvAlloc(size);
|
|
|
|
if (!picture_buf)
|
|
|
|
{
|
|
|
|
av_free(picture);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
avpicture_fill((AVPicture *)picture, picture_buf,
|
|
|
|
(PixelFormat) pix_fmt, width, height);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
}
|
|
|
|
return picture;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* add a video output stream to the container */
|
|
|
|
static AVStream *icv_add_video_stream_FFMPEG(AVFormatContext *oc,
|
|
|
|
CodecID codec_id,
|
|
|
|
int w, int h, int bitrate,
|
|
|
|
double fps, int pixel_format)
|
|
|
|
{
|
|
|
|
AVCodecContext *c;
|
|
|
|
AVStream *st;
|
|
|
|
int frame_rate, frame_rate_base;
|
|
|
|
AVCodec *codec;
|
|
|
|
|
|
|
|
|
|
|
|
st = av_new_stream(oc, 0);
|
|
|
|
if (!st) {
|
|
|
|
CV_WARN("Could not allocate stream");
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
c = st->codec;
|
|
|
|
#else
|
|
|
|
c = &(st->codec);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4621
|
|
|
|
c->codec_id = av_guess_codec(oc->oformat, NULL, oc->filename, NULL, CODEC_TYPE_VIDEO);
|
|
|
|
#else
|
|
|
|
c->codec_id = oc->oformat->video_codec;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if(codec_id != CODEC_ID_NONE){
|
|
|
|
c->codec_id = codec_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
//if(codec_tag) c->codec_tag=codec_tag;
|
|
|
|
codec = avcodec_find_encoder(c->codec_id);
|
|
|
|
|
|
|
|
c->codec_type = CODEC_TYPE_VIDEO;
|
|
|
|
|
|
|
|
/* put sample parameters */
|
|
|
|
c->bit_rate = bitrate;
|
|
|
|
|
|
|
|
/* resolution must be a multiple of two */
|
|
|
|
c->width = w;
|
|
|
|
c->height = h;
|
|
|
|
|
|
|
|
/* time base: this is the fundamental unit of time (in seconds) in terms
|
|
|
|
of which frame timestamps are represented. for fixed-fps content,
|
|
|
|
timebase should be 1/framerate and timestamp increments should be
|
|
|
|
identically 1. */
|
|
|
|
frame_rate=cvRound(fps);
|
|
|
|
frame_rate_base=1;
|
|
|
|
while (fabs((double)frame_rate/frame_rate_base) - fps > 0.001){
|
|
|
|
frame_rate_base*=10;
|
|
|
|
frame_rate=cvRound(fps*frame_rate_base);
|
|
|
|
}
|
|
|
|
#if LIBAVFORMAT_BUILD > 4752
|
|
|
|
c->time_base.den = frame_rate;
|
|
|
|
c->time_base.num = frame_rate_base;
|
|
|
|
/* adjust time base for supported framerates */
|
|
|
|
if(codec && codec->supported_framerates){
|
|
|
|
const AVRational *p= codec->supported_framerates;
|
|
|
|
AVRational req = {frame_rate, frame_rate_base};
|
|
|
|
const AVRational *best=NULL;
|
|
|
|
AVRational best_error= {INT_MAX, 1};
|
|
|
|
for(; p->den!=0; p++){
|
|
|
|
AVRational error= av_sub_q(req, *p);
|
|
|
|
if(error.num <0) error.num *= -1;
|
|
|
|
if(av_cmp_q(error, best_error) < 0){
|
|
|
|
best_error= error;
|
|
|
|
best= p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
c->time_base.den= best->num;
|
|
|
|
c->time_base.num= best->den;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
c->frame_rate = frame_rate;
|
|
|
|
c->frame_rate_base = frame_rate_base;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
c->gop_size = 12; /* emit one intra frame every twelve frames at most */
|
|
|
|
c->pix_fmt = (PixelFormat) pixel_format;
|
|
|
|
|
|
|
|
if (c->codec_id == CODEC_ID_MPEG2VIDEO) {
|
|
|
|
c->max_b_frames = 2;
|
|
|
|
}
|
|
|
|
if (c->codec_id == CODEC_ID_MPEG1VIDEO || c->codec_id == CODEC_ID_MSMPEG4V3){
|
|
|
|
/* needed to avoid using macroblocks in which some coeffs overflow
|
|
|
|
this doesnt happen with normal video, it just happens here as the
|
|
|
|
motion of the chroma plane doesnt match the luma plane */
|
|
|
|
/* avoid FFMPEG warning 'clipping 1 dct coefficients...' */
|
|
|
|
c->mb_decision=2;
|
|
|
|
}
|
|
|
|
#if LIBAVCODEC_VERSION_INT>0x000409
|
|
|
|
// some formats want stream headers to be seperate
|
|
|
|
if(oc->oformat->flags & AVFMT_GLOBALHEADER)
|
|
|
|
{
|
|
|
|
c->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
return st;
|
|
|
|
}
|
|
|
|
|
|
|
|
int icv_av_write_frame_FFMPEG( AVFormatContext * oc, AVStream * video_st, uint8_t * outbuf, uint32_t outbuf_size, AVFrame * picture ){
|
|
|
|
CV_FUNCNAME("icv_av_write_frame_FFMPEG");
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
AVCodecContext * c = video_st->codec;
|
|
|
|
#else
|
|
|
|
AVCodecContext * c = &(video_st->codec);
|
|
|
|
#endif
|
|
|
|
int out_size;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
__BEGIN__;
|
|
|
|
|
|
|
|
if (oc->oformat->flags & AVFMT_RAWPICTURE) {
|
|
|
|
/* raw video case. The API will change slightly in the near
|
|
|
|
futur for that */
|
|
|
|
AVPacket pkt;
|
|
|
|
av_init_packet(&pkt);
|
|
|
|
|
|
|
|
pkt.flags |= PKT_FLAG_KEY;
|
|
|
|
pkt.stream_index= video_st->index;
|
|
|
|
pkt.data= (uint8_t *)picture;
|
|
|
|
pkt.size= sizeof(AVPicture);
|
|
|
|
|
|
|
|
ret = av_write_frame(oc, &pkt);
|
|
|
|
} else {
|
|
|
|
/* encode the image */
|
|
|
|
out_size = avcodec_encode_video(c, outbuf, outbuf_size, picture);
|
|
|
|
/* if zero size, it means the image was buffered */
|
|
|
|
if (out_size > 0) {
|
|
|
|
AVPacket pkt;
|
|
|
|
av_init_packet(&pkt);
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4752
|
|
|
|
pkt.pts = av_rescale_q(c->coded_frame->pts, c->time_base, video_st->time_base);
|
|
|
|
#else
|
|
|
|
pkt.pts = c->coded_frame->pts;
|
|
|
|
#endif
|
|
|
|
if(c->coded_frame->key_frame)
|
|
|
|
pkt.flags |= PKT_FLAG_KEY;
|
|
|
|
pkt.stream_index= video_st->index;
|
|
|
|
pkt.data= outbuf;
|
|
|
|
pkt.size= out_size;
|
|
|
|
|
|
|
|
/* write the compressed frame in the media file */
|
|
|
|
ret = av_write_frame(oc, &pkt);
|
|
|
|
} else {
|
|
|
|
ret = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (ret != 0) {
|
|
|
|
CV_ERROR(CV_StsError, "Error while writing video frame");
|
|
|
|
}
|
|
|
|
|
|
|
|
__END__;
|
|
|
|
return CV_StsOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// write a frame with FFMPEG
|
|
|
|
bool CvVideoWriter_FFMPEG::writeFrame( const IplImage * image )
|
|
|
|
{
|
|
|
|
bool ret = false;
|
|
|
|
|
|
|
|
CV_FUNCNAME("CvVideoWriter_FFMPEG::writerFrame");
|
|
|
|
|
|
|
|
__BEGIN__;
|
|
|
|
|
|
|
|
// typecast from opaque data type to implemented struct
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
AVCodecContext *c = video_st->codec;
|
|
|
|
#else
|
|
|
|
AVCodecContext *c = &(video_st->codec);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD < 5231
|
|
|
|
// It is not needed in the latest versions of the ffmpeg
|
|
|
|
if( c->codec_id == CODEC_ID_RAWVIDEO && image->origin != IPL_ORIGIN_BL )
|
|
|
|
{
|
|
|
|
if( !temp_image )
|
|
|
|
temp_image = cvCreateImage( cvGetSize(image),
|
|
|
|
image->depth, image->nChannels );
|
|
|
|
cvFlip( image, temp_image, 0 );
|
|
|
|
image = temp_image;
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// check parameters
|
|
|
|
if (input_pix_fmt == PIX_FMT_BGR24) {
|
|
|
|
if (image->nChannels != 3 || image->depth != IPL_DEPTH_8U) {
|
|
|
|
CV_ERROR(CV_StsUnsupportedFormat, "cvWriteFrame() needs images with depth = IPL_DEPTH_8U and nChannels = 3.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (input_pix_fmt == PIX_FMT_GRAY8) {
|
|
|
|
if (image->nChannels != 1 || image->depth != IPL_DEPTH_8U) {
|
|
|
|
CV_ERROR(CV_StsUnsupportedFormat, "cvWriteFrame() needs images with depth = IPL_DEPTH_8U and nChannels = 1.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
assert(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if buffer sizes match, i.e. image has expected format (size, channels, bitdepth, alignment)
|
|
|
|
#if LIBAVCODEC_VERSION_INT >= ((52<<16)+(37<<8)+0)
|
|
|
|
assert (image->imageSize == avpicture_get_size( (PixelFormat)input_pix_fmt, image->width, image->height ));
|
|
|
|
#else
|
|
|
|
assert (image->imageSize == avpicture_get_size( input_pix_fmt, image->width, image->height ));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
if ( c->pix_fmt != input_pix_fmt ) {
|
|
|
|
assert( input_picture );
|
|
|
|
// let input_picture point to the raw data buffer of 'image'
|
|
|
|
avpicture_fill((AVPicture *)input_picture, (uint8_t *) image->imageData,
|
|
|
|
(PixelFormat)input_pix_fmt, image->width, image->height);
|
|
|
|
|
|
|
|
#if !defined(HAVE_FFMPEG_SWSCALE)
|
|
|
|
// convert to the color format needed by the codec
|
|
|
|
if( img_convert((AVPicture *)picture, c->pix_fmt,
|
|
|
|
(AVPicture *)input_picture, (PixelFormat)input_pix_fmt,
|
|
|
|
image->width, image->height) < 0){
|
|
|
|
CV_ERROR(CV_StsUnsupportedFormat, "FFMPEG::img_convert pixel format conversion from BGR24 not handled");
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
img_convert_ctx = sws_getContext(image->width,
|
|
|
|
image->height,
|
|
|
|
PIX_FMT_BGR24,
|
|
|
|
c->width,
|
|
|
|
c->height,
|
|
|
|
c->pix_fmt,
|
|
|
|
SWS_BICUBIC,
|
|
|
|
NULL, NULL, NULL);
|
|
|
|
|
|
|
|
if ( sws_scale(img_convert_ctx, input_picture->data,
|
|
|
|
input_picture->linesize, 0,
|
|
|
|
image->height,
|
|
|
|
picture->data, picture->linesize) < 0 )
|
|
|
|
{
|
|
|
|
CV_ERROR(CV_StsUnsupportedFormat, "FFMPEG::img_convert pixel format conversion from BGR24 not handled");
|
|
|
|
}
|
|
|
|
sws_freeContext(img_convert_ctx);
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
avpicture_fill((AVPicture *)picture, (uint8_t *) image->imageData,
|
|
|
|
(PixelFormat)input_pix_fmt, image->width, image->height);
|
|
|
|
}
|
|
|
|
|
|
|
|
ret = icv_av_write_frame_FFMPEG( oc, video_st, outbuf, outbuf_size, picture) >= 0;
|
|
|
|
|
|
|
|
__END__;
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// close video output stream and free associated memory
|
|
|
|
void CvVideoWriter_FFMPEG::close()
|
|
|
|
{
|
|
|
|
unsigned i;
|
|
|
|
|
|
|
|
// nothing to do if already released
|
|
|
|
if ( !picture )
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* no more frame to compress. The codec has a latency of a few
|
|
|
|
frames if using B frames, so we get the last frames by
|
|
|
|
passing the same picture again */
|
|
|
|
// TODO -- do we need to account for latency here?
|
|
|
|
|
|
|
|
/* write the trailer, if any */
|
|
|
|
av_write_trailer(oc);
|
|
|
|
|
|
|
|
// free pictures
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
if( video_st->codec->pix_fmt != input_pix_fmt){
|
|
|
|
#else
|
|
|
|
if( video_st->codec.pix_fmt != input_pix_fmt){
|
|
|
|
#endif
|
|
|
|
cvFree(&(picture->data[0]));
|
|
|
|
}
|
|
|
|
av_free(picture);
|
|
|
|
|
|
|
|
if (input_picture) {
|
|
|
|
av_free(input_picture);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* close codec */
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
avcodec_close(video_st->codec);
|
|
|
|
#else
|
|
|
|
avcodec_close(&(video_st->codec));
|
|
|
|
#endif
|
|
|
|
|
|
|
|
av_free(outbuf);
|
|
|
|
|
|
|
|
/* free the streams */
|
|
|
|
for(i = 0; i < oc->nb_streams; i++) {
|
|
|
|
av_freep(&oc->streams[i]->codec);
|
|
|
|
av_freep(&oc->streams[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(fmt->flags & AVFMT_NOFILE)) {
|
|
|
|
/* close the output file */
|
|
|
|
|
|
|
|
|
|
|
|
#if LIBAVCODEC_VERSION_INT >= ((51<<16)+(49<<8)+0)
|
|
|
|
url_fclose(oc->pb);
|
|
|
|
#else
|
|
|
|
url_fclose(&oc->pb);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/* free the stream */
|
|
|
|
av_free(oc);
|
|
|
|
|
|
|
|
cvReleaseImage( &temp_image );
|
|
|
|
|
|
|
|
init();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create a video writer object that uses FFMPEG
|
|
|
|
bool CvVideoWriter_FFMPEG::open( const char * filename, int fourcc,
|
|
|
|
double fps, CvSize frameSize, bool is_color )
|
|
|
|
{
|
|
|
|
CodecID codec_id = CODEC_ID_NONE;
|
|
|
|
int err, codec_pix_fmt, bitrate_scale=64;
|
|
|
|
|
|
|
|
close();
|
|
|
|
|
|
|
|
// check arguments
|
|
|
|
assert (filename);
|
|
|
|
assert (fps > 0);
|
|
|
|
assert (frameSize.width > 0 && frameSize.height > 0);
|
|
|
|
|
|
|
|
// tell FFMPEG to register codecs
|
|
|
|
av_register_all ();
|
|
|
|
|
|
|
|
/* auto detect the output format from the name and fourcc code. */
|
|
|
|
fmt = guess_format(NULL, filename, NULL);
|
|
|
|
if (!fmt)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
/* determine optimal pixel format */
|
|
|
|
if (is_color) {
|
|
|
|
input_pix_fmt = PIX_FMT_BGR24;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
input_pix_fmt = PIX_FMT_GRAY8;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Lookup codec_id for given fourcc */
|
|
|
|
#if LIBAVCODEC_VERSION_INT<((51<<16)+(49<<8)+0)
|
|
|
|
if( (codec_id = codec_get_bmp_id( fourcc )) == CODEC_ID_NONE )
|
|
|
|
return false;
|
|
|
|
#else
|
|
|
|
const struct AVCodecTag * tags[] = { codec_bmp_tags, NULL};
|
|
|
|
if( (codec_id = av_codec_get_id(tags, fourcc)) == CODEC_ID_NONE )
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// alloc memory for context
|
|
|
|
oc = av_alloc_format_context();
|
|
|
|
assert (oc);
|
|
|
|
|
|
|
|
/* set file name */
|
|
|
|
oc->oformat = fmt;
|
|
|
|
snprintf(oc->filename, sizeof(oc->filename), "%s", filename);
|
|
|
|
|
|
|
|
/* set some options */
|
|
|
|
oc->max_delay = (int)(0.7*AV_TIME_BASE); /* This reduces buffer underrun warnings with MPEG */
|
|
|
|
|
|
|
|
// set a few optimal pixel formats for lossless codecs of interest..
|
|
|
|
switch (codec_id) {
|
|
|
|
#if LIBAVCODEC_VERSION_INT>((50<<16)+(1<<8)+0)
|
|
|
|
case CODEC_ID_JPEGLS:
|
|
|
|
// BGR24 or GRAY8 depending on is_color...
|
|
|
|
codec_pix_fmt = input_pix_fmt;
|
|
|
|
break;
|
|
|
|
#endif
|
|
|
|
case CODEC_ID_HUFFYUV:
|
|
|
|
codec_pix_fmt = PIX_FMT_YUV422P;
|
|
|
|
break;
|
|
|
|
case CODEC_ID_MJPEG:
|
|
|
|
case CODEC_ID_LJPEG:
|
|
|
|
codec_pix_fmt = PIX_FMT_YUVJ420P;
|
|
|
|
bitrate_scale = 128;
|
|
|
|
break;
|
|
|
|
case CODEC_ID_RAWVIDEO:
|
|
|
|
default:
|
|
|
|
// good for lossy formats, MPEG, etc.
|
|
|
|
codec_pix_fmt = PIX_FMT_YUV420P;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO -- safe to ignore output audio stream?
|
|
|
|
video_st = icv_add_video_stream_FFMPEG(oc, codec_id,
|
|
|
|
frameSize.width, frameSize.height, frameSize.width*frameSize.height*bitrate_scale,
|
|
|
|
fps, codec_pix_fmt);
|
|
|
|
|
|
|
|
|
|
|
|
/* set the output parameters (must be done even if no
|
|
|
|
parameters). */
|
|
|
|
if (av_set_parameters(oc, NULL) < 0) {
|
|
|
|
CV_Error(CV_StsBadArg, "Invalid output format parameters");
|
|
|
|
}
|
|
|
|
|
|
|
|
dump_format(oc, 0, filename, 1);
|
|
|
|
|
|
|
|
/* now that all the parameters are set, we can open the audio and
|
|
|
|
video codecs and allocate the necessary encode buffers */
|
|
|
|
if (!video_st){
|
|
|
|
CV_Error(CV_StsBadArg, "Couldn't open video stream");
|
|
|
|
}
|
|
|
|
|
|
|
|
AVCodec *codec;
|
|
|
|
AVCodecContext *c;
|
|
|
|
|
|
|
|
#if LIBAVFORMAT_BUILD > 4628
|
|
|
|
c = (video_st->codec);
|
|
|
|
#else
|
|
|
|
c = &(video_st->codec);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
c->codec_tag = fourcc;
|
|
|
|
/* find the video encoder */
|
|
|
|
codec = avcodec_find_encoder(c->codec_id);
|
|
|
|
if (!codec) {
|
|
|
|
CV_Error(CV_StsBadArg, "codec not found");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* open the codec */
|
|
|
|
if ( (err=avcodec_open(c, codec)) < 0) {
|
|
|
|
char errtext[256];
|
|
|
|
sprintf(errtext, "Could not open codec '%s': %s", codec->name, icvFFMPEGErrStr(err));
|
|
|
|
CV_Error(CV_StsBadArg, errtext);
|
|
|
|
}
|
|
|
|
|
|
|
|
outbuf = NULL;
|
|
|
|
|
|
|
|
if (!(oc->oformat->flags & AVFMT_RAWPICTURE)) {
|
|
|
|
/* allocate output buffer */
|
|
|
|
/* assume we will never get codec output with more than 4 bytes per pixel... */
|
|
|
|
outbuf_size = frameSize.width*frameSize.height*4;
|
|
|
|
outbuf = (uint8_t *) av_malloc(outbuf_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool need_color_convert;
|
|
|
|
need_color_convert = (c->pix_fmt != input_pix_fmt);
|
|
|
|
|
|
|
|
/* allocate the encoded raw picture */
|
|
|
|
picture = icv_alloc_picture_FFMPEG(c->pix_fmt, c->width, c->height, need_color_convert);
|
|
|
|
if (!picture) {
|
|
|
|
CV_Error(CV_StsNoMem, "Could not allocate picture");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* if the output format is not our input format, then a temporary
|
|
|
|
picture of the input format is needed too. It is then converted
|
|
|
|
to the required output format */
|
|
|
|
input_picture = NULL;
|
|
|
|
if ( need_color_convert ) {
|
|
|
|
input_picture = icv_alloc_picture_FFMPEG(input_pix_fmt, c->width, c->height, false);
|
|
|
|
if (!input_picture) {
|
|
|
|
CV_Error(CV_StsNoMem, "Could not allocate picture");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* open the output file, if needed */
|
|
|
|
if (!(fmt->flags & AVFMT_NOFILE)) {
|
|
|
|
if (url_fopen(&oc->pb, filename, URL_WRONLY) < 0) {
|
|
|
|
CV_Error(CV_StsBadArg, "Couldn't open output file for writing");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* write the stream header, if any */
|
|
|
|
av_write_header( oc );
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
CvVideoWriter* cvCreateVideoWriter_FFMPEG( const char* filename, int fourcc, double fps,
|
|
|
|
CvSize frameSize, int isColor )
|
|
|
|
{
|
|
|
|
CvVideoWriter_FFMPEG* writer = new CvVideoWriter_FFMPEG;
|
|
|
|
if( writer->open( filename, fourcc, fps, frameSize, isColor != 0 ))
|
|
|
|
return writer;
|
|
|
|
delete writer;
|
|
|
|
return 0;
|
|
|
|
}
|