From f238d1fd97cb605da60bbad619baa215d4569c32 Mon Sep 17 00:00:00 2001 From: tobigun Date: Thu, 25 Nov 2010 11:05:44 +0000 Subject: move /src/plugins/media to src/mediaplugins and /game/plugins/media to /game/mediaplugins git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/branches/experimental@2753 b956fd51-792f-4845-bead-9b4dfca2ff2c --- .../mediaplugins/ffmpeg/ffmpeg_video_decode.cpp | 662 +++++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 mediaplugin/src/mediaplugins/ffmpeg/ffmpeg_video_decode.cpp (limited to 'mediaplugin/src/mediaplugins/ffmpeg/ffmpeg_video_decode.cpp') diff --git a/mediaplugin/src/mediaplugins/ffmpeg/ffmpeg_video_decode.cpp b/mediaplugin/src/mediaplugins/ffmpeg/ffmpeg_video_decode.cpp new file mode 100644 index 00000000..b3fc77b3 --- /dev/null +++ b/mediaplugin/src/mediaplugins/ffmpeg/ffmpeg_video_decode.cpp @@ -0,0 +1,662 @@ +/* UltraStar Deluxe - Karaoke Game + * + * UltraStar Deluxe is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + * + * $URL$ + * $Id$ + */ + +/* + * based on 'An ffmpeg and SDL Tutorial' (http://www.dranger.com/ffmpeg/) + */ + +#include "ffmpeg_video_decode.h" +#include +#include + +// These are called whenever we allocate a frame buffer. +// We use this to store the global_pts in a frame at the time it is allocated. +int CDECL ptsGetBuffer(AVCodecContext *codecCtx, AVFrame *frame) { + int result = avcodec_default_get_buffer(codecCtx, frame); + int64_t *videoPktPts = (int64_t*)codecCtx->opaque; + if (videoPktPts) { + // Note: we must copy the pts instead of passing a pointer, because the packet + // (and with it the pts) might change before a frame is returned by av_decode_video. + int64_t *pts = (int64_t*)av_malloc(sizeof(int64_t)); + *pts = *videoPktPts; + frame->opaque = pts; + } + return result; +} + +void CDECL ptsReleaseBuffer(AVCodecContext *codecCtx, AVFrame *frame) { + if (frame) + av_freep(&frame->opaque); + avcodec_default_release_buffer(codecCtx, frame); +} + +/* + * TVideoDecoder_FFmpeg + */ + +FFmpegVideoDecodeStream::FFmpegVideoDecodeStream() : + _opened(false), + _eof(false), + _loop(false), + _stream(NULL), + _streamIndex(-1), + _formatContext(NULL), + _codecContext(NULL), + _codec(NULL), + _avFrame(NULL), + _avFrameRGB(NULL), + _frameBuffer(NULL), + _frameTexValid(false), +#ifdef USE_SWSCALE + _swScaleContext(NULL), +#endif + _aspect(0), + _frameDuration(0), + _frameTime(0), + _loopTime(0) {} + +FFmpegVideoDecodeStream* FFmpegVideoDecodeStream::open(const IPath &filename) { + FFmpegVideoDecodeStream *stream = new FFmpegVideoDecodeStream(); + if (!stream->_open(filename)) { + delete stream; + return 0; + } + return stream; +} + +bool FFmpegVideoDecodeStream::_open(const IPath &filename) { + std::stringstream ss; + + // use custom 'ufile' protocol for UTF-8 support + int errnum = av_open_input_file(&_formatContext, + ("ufile:" + filename.toUTF8()).c_str(), NULL, 0, NULL); + if (errnum != 0) { + logger.error("Failed to open file '" + filename.toNative() + "' (" + + ffmpegCore->getErrorString(errnum) + ")", + "VideoDecodeStream_FFmpeg::Open"); + return false; + } + + // update video info + if (av_find_stream_info(_formatContext) < 0) { + logger.error("No stream info found", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + ss.str(""); + ss << "VideoStreamIndex: " << _streamIndex; + logger.info(ss.str(), "VideoPlayback_ffmpeg.Open"); + + // find video stream + int audioStreamIndex; + ffmpegCore->findStreamIDs(_formatContext, &_streamIndex, &audioStreamIndex); + if (_streamIndex < 0) { + logger.error("No video stream found", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + _stream = _formatContext->streams[_streamIndex]; + _codecContext = _stream->codec; + + _codec = avcodec_find_decoder(_codecContext->codec_id); + if (!_codec) { + logger.error("No matching codec found", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + // set debug options + _codecContext->debug_mv = 0; + _codecContext->debug = 0; + + // detect bug-workarounds automatically + _codecContext->workaround_bugs = FF_BUG_AUTODETECT; + // error resilience strategy (careful/compliant/agressive/very_aggressive) + //fCodecContext->error_resilience = FF_ER_CAREFUL; //FF_ER_COMPLIANT; + // allow non spec compliant speedup tricks. + //fCodecContext->flags2 = fCodecContext->flags2 | CODEC_FLAG2_FAST; + + // Note: avcodec_open() and avcodec_close() are not thread-safe and will + // fail if called concurrently by different threads. + { + MediaCore_FFmpeg::AVCodecLock codecLock; + errnum = avcodec_open(_codecContext, _codec); + } + if (errnum < 0) { + logger.error("No matching codec found", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + // register custom callbacks for pts-determination + _codecContext->get_buffer = ptsGetBuffer; + _codecContext->release_buffer = ptsReleaseBuffer; + +#ifdef DEBUG_DISPLAY + ss.str(""); + ss << "Found a matching Codec: " << _codecContext->codec->name << std::endl + << std::endl + << " Width = " << _codecContext->width + << ", Height=" << _codecContext->height << std::endl + << " Aspect : " << _codecContext->sample_aspect_ratio.num << "/" + << _codecContext->sample_aspect_ratio.den << std::endl + << " Framerate : " << _codecContext->time_base.num << "/" + << _codecContext->time_base.den; + logger.status(ss.str(), ""); +#endif + + // allocate space for decoded frame and rgb frame + _avFrame = avcodec_alloc_frame(); + _avFrameRGB = avcodec_alloc_frame(); + _frameBuffer = (uint8_t*) av_malloc(avpicture_get_size(PIXEL_FMT_FFMPEG, + _codecContext->width, _codecContext->height)); + + if (!_avFrame || !_avFrameRGB || !_frameBuffer) { + logger.error("Failed to allocate buffers", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + // TODO: pad data for OpenGL to GL_UNPACK_ALIGNMENT + // (otherwise video will be distorted if width/height is not a multiple of the alignment) + errnum = avpicture_fill((AVPicture*)_avFrameRGB, _frameBuffer, + PIXEL_FMT_FFMPEG, _codecContext->width, _codecContext->height); + if (errnum < 0) { + logger.error("avpicture_fill failed: " + ffmpegCore->getErrorString(errnum), + "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } + + // calculate some information for video display + _aspect = av_q2d(_codecContext->sample_aspect_ratio); + if (_aspect == 0) { + _aspect = (double)_codecContext->width / _codecContext->height; + } else { + _aspect *= (double)_codecContext->width / _codecContext->height; + } + + _frameDuration = 1.0 / av_q2d(_stream->r_frame_rate); + + // hack to get reasonable framerate (for divx and others) + if (_frameDuration < 0.02) { // 0.02 <-> 50 fps + _frameDuration = av_q2d(_stream->r_frame_rate); + while (_frameDuration > 50.0) + _frameDuration /= 10.0; + _frameDuration = 1.0 / _frameDuration; + } + + ss.str(""); + ss << "Framerate: " << (int)(1.0 / _frameDuration) << "fps"; + logger.info(ss.str(), "VideoPlayback_ffmpeg.Open"); + +#ifdef USE_SWSCALE + // if available get a SWScale-context -> faster than the deprecated img_convert(). + // SWScale has accelerated support for PIX_FMT_RGB32/PIX_FMT_BGR24/PIX_FMT_BGR565/PIX_FMT_BGR555. + // Note: PIX_FMT_RGB32 is a BGR- and not an RGB-format (maybe a bug)!!! + // The BGR565-formats (GL_UNSIGNED_SHORT_5_6_5) is way too slow because of its + // bad OpenGL support. The BGR formats have MMX(2) implementations but no speed-up + // could be observed in comparison to the RGB versions. + _swScaleContext = sws_getCachedContext(NULL, + _codecContext->width, _codecContext->height, _codecContext->pix_fmt, + _codecContext->width, _codecContext->height, PIXEL_FMT_FFMPEG, + SWS_FAST_BILINEAR, + NULL, NULL, NULL); + if (!_swScaleContext) { + logger.error("Failed to get swscale context", "VideoPlayback_ffmpeg.Open"); + close(); + return false; + } +#endif + + _opened = true; + return true; +} + +void FFmpegVideoDecodeStream::close() { + if (_frameBuffer) + av_free(_frameBuffer); + if (_avFrameRGB) + av_free(_avFrameRGB); + if (_avFrame) + av_free(_avFrame); + + _avFrame = NULL; + _avFrameRGB = NULL; + _frameBuffer = NULL; + + if (_codecContext) { + // avcodec_close() is not thread-safe + MediaCore_FFmpeg::AVCodecLock codecLock; + avcodec_close(_codecContext); + } + + if (_formatContext) + av_close_input_file(_formatContext); + + _codecContext = NULL; + _formatContext = NULL; + + _opened = false; +} + +void FFmpegVideoDecodeStream::synchronizeTime(AVFrame *frame, double &pts) { + if (pts != 0) { + // if we have pts, set video clock to it + _frameTime = pts; + } else { + // if we aren't given a pts, set it to the clock + pts = _frameTime; + } + // update the video clock + double frameDelay = av_q2d(_codecContext->time_base); + // if we are repeating a frame, adjust clock accordingly + frameDelay = frameDelay + frame->repeat_pict * (frameDelay * 0.5); + _frameTime = _frameTime + frameDelay; +} + +/** + * Decode a new frame from the video stream. + * The decoded frame is stored in fAVFrame. fFrameTime is updated to the new frame's + * time. + * @param pts will be updated to the presentation time of the decoded frame. + * returns true if a frame could be decoded. False if an error or EOF occured. + */ +bool FFmpegVideoDecodeStream::decodeFrame() { + int64_t videoPktPts; + ByteIOContext *pbIOCtx; + int errnum; + AVPacket packet; + double pts; + + if (_eof) + return false; + + // read packets until we have a finished frame (or there are no more packets) + int frameFinished = 0; + while (frameFinished == 0) { + errnum = av_read_frame(_formatContext, &packet); + if (errnum < 0) { + // failed to read a frame, check reason + +#if LIBAVFORMAT_VERSION_MAJOR >= 52 + pbIOCtx = _formatContext->pb; +#else + pbIOCtx = &_formatContext->pb; +#endif + + // check for end-of-file (EOF is not an error) + if (url_feof(pbIOCtx) != 0) { + _eof = true; + return false; + } + + // check for errors + if (url_ferror(pbIOCtx) != 0) { + logger.error("Video decoding file error", "TVideoPlayback_FFmpeg.DecodeFrame"); + return false; + } + + // url_feof() does not detect an EOF for some mov-files (e.g. deluxe.mov) + // so we have to do it this way. + if ((_formatContext->file_size != 0) && + (pbIOCtx->pos >= _formatContext->file_size)) + { + _eof = true; + return false; + } + + // error occured, log and exit + logger.error("Video decoding error", "TVideoPlayback_FFmpeg.DecodeFrame"); + return false; + } + + // if we got a packet from the video stream, then decode it + if (packet.stream_index == _streamIndex) { + // save pts to be stored in pFrame in first call of PtsGetBuffer() + videoPktPts = packet.pts; + // FIXME: is the pointer valid when it is used? + _codecContext->opaque = &videoPktPts; + + // decode packet + avcodec_decode_video2(_codecContext, _avFrame, &frameFinished, &packet); + + // reset opaque data + _codecContext->opaque = NULL; + + // update pts + if (packet.dts != (int64_t)AV_NOPTS_VALUE) { + pts = packet.dts; + } else if (_avFrame->opaque && + (*((int64_t*)_avFrame->opaque) != (int64_t)AV_NOPTS_VALUE)) + { + pts = *((int64_t*)_avFrame->opaque); + } else { + pts = 0; + } + + if (_stream->start_time != (int64_t)AV_NOPTS_VALUE) + pts -= _stream->start_time; + + pts *= av_q2d(_stream->time_base); + + // synchronize time on each complete frame + if (frameFinished != 0) + synchronizeTime(_avFrame, pts); + } + + // free the packet from av_read_frame + av_free_packet(&packet); + } + + return true; +} + +#ifdef DEBUG_FRAMES +void spawnGoldenRec(double x, double y, int screen, uint8_t live, int startFrame, + int recArrayIndex, unsigned player) +{ + //GoldenRec.Spawn(x, y, screen, live, startFrame, recArrayIndex, ColoredStar, player) +} +#endif + +uint8_t* FFmpegVideoDecodeStream::getFrame(long double time) { + const long double SKIP_FRAME_DIFF = 0.010; // start skipping if we are >= 10ms too late + std::stringstream ss; + + if (!_opened) + return NULL; + + /* + * Synchronization - begin + */ + + // requested stream position (relative to the last loop's start) + long double currentTime; + if (_loop) + currentTime = time - _loopTime; + else + currentTime = time; + + // check if current texture still contains the active frame + if (_frameTexValid) { + // time since the last frame was returned + long double timeDiff = currentTime - _frameTime; + +#ifdef DEBUG_DISPLAY + ss.str(""); + ss << "time: " << floor(time*1000) << std::endl + << "VideoTime: " << floor(_frameTime*1000) << std::endl + << "TimeBase: " << floor(_frameDuration*1000) << std::endl + << "timeDiff: " << floor(timeDiff*1000); + logger.status(ss.str(), ""); +#endif + + // check if time has reached the next frame + if (timeDiff < _frameDuration) { +#ifdef DEBUG_FRAMES + // frame delay debug display + spawnGoldenRec(200, 15, 1, 16, 0, -1, 0x00ff00); +#endif + +#ifdef DEBUG_DISPLAY + ss.str(""); + ss << "not getting new frame" << std::endl + << "time: " << floor(time*1000) << std::endl + << "VideoTime: " << floor(_frameTime*1000) << std::endl + << "TimeBase: " << floor(_frameDuration*1000) << std::endl + << "timeDiff: " << floor(timeDiff*1000); + logger.status(ss.str(), ""); +#endif + + // we do not need a new frame now + return NULL; + } + } + + // fetch new frame (updates fFrameTime) + bool success = decodeFrame(); + long double timeDiff = currentTime - _frameTime; + + // check if we have to skip frames + // Either if we are one frame behind or if the skip threshold has been reached. + // Do not skip if the difference is less than fFrameDuration as there is no next frame. + // Note: We assume that fFrameDuration is the length of one frame. + if (timeDiff >= std::max(_frameDuration, SKIP_FRAME_DIFF)) { +#ifdef DEBUG_FRAMES + //frame drop debug display + spawnGoldenRec(200, 55, 1, 16, 0, -1, 0xff0000); +#endif +#ifdef DEBUG_DISPLAY + ss.str(""); + ss << "skipping frames" << std::endl + << "TimeBase: " << floor(_frameDuration*1000) << std::endl + << "timeDiff: " << floor(timeDiff*1000); + logger.status(ss.str(), ""); +#endif + + // update video-time + int dropFrameCount = (int)(timeDiff / _frameDuration); + _frameTime = _frameTime + dropFrameCount * _frameDuration; + + // skip frames + for (int i = 1; i <= dropFrameCount; ++i) + success = decodeFrame(); + } + + // check if we got an EOF or error + if (!success) { + if (_loop) { + // we have to loop, so rewind + setPosition(0); + // record the start-time of the current loop, so we can + // determine the position in the stream (fFrameTime-fLoopTime) later. + _loopTime = time; + } + return NULL; + } + + /* + * Synchronization - end + */ + + // TODO: support for pan&scan + //if (_avFrame->pan_scan) { + // printf("PanScan: %d/%d", _avFrame->pan_scan->width, _avFrame->pan_scan->height); + //} + + // otherwise we convert the pixeldata from YUV to RGB + int errnum; +#ifdef USE_SWSCALE + errnum = sws_scale(_swScaleContext, + (uint8_t**)_avFrame->data, _avFrame->linesize, + 0, _codecContext->height, + (uint8_t**)_avFrameRGB->data, _avFrameRGB->linesize); +#else + // img_convert from lib/ffmpeg/avcodec.pas is actually deprecated. + // If ./configure does not find SWScale then this gives the error + // that the identifier img_convert is not known or similar. + // I think this should be removed, but am not sure whether there should + // be some other replacement or a warning, Therefore, I leave it for now. + // April 2009, mischi + errnum = img_convert((AVPicture*)_avFrameRGB, PIXEL_FMT_FFMPEG, + (AVPicture*)_avFrame, _codecContext->pix_fmt, + _codecContext->width, _codecContext->height); +#endif + + if (errnum < 0) { + logger.error("Image conversion failed", "TVideoPlayback_ffmpeg.GetFrame"); + return NULL; + } + + if (!_frameTexValid) + _frameTexValid = true; + + return _avFrameRGB->data[0]; +} + +void FFmpegVideoDecodeStream::setLoop(bool enable) { + _loop = enable; + _loopTime = 0; +} + +bool FFmpegVideoDecodeStream::getLoop() { + return _loop; +} + +/** + * Sets the stream's position. + * The stream is set to the first keyframe with timestamp <= Time. + * Note that fFrameTime is set to Time no matter if the actual position seeked to is + * at Time or the time of a preceding keyframe. fFrameTime will be updated to the + * actual frame time when GetFrame() is called the next time. + * @param Time new position in seconds + */ +void FFmpegVideoDecodeStream::setPosition(double time) { + int seekFlags; + + if (!_opened) + return; + + if (time < 0) + time = 0; + + // TODO: handle fLoop-times + //time %= videoDuration; + + // Do not use the AVSEEK_FLAG_ANY here. It will seek to any frame, even + // non keyframes (P-/B-frames). It will produce corrupted video frames as + // FFmpeg does not use the information of the preceding I-frame. + // The picture might be gray or green until the next keyframe occurs. + // Instead seek the first keyframe smaller than the requested time + // (AVSEEK_FLAG_BACKWARD). As this can be some seconds earlier than the + // requested time, let the sync in GetFrame() do its job. + seekFlags = AVSEEK_FLAG_BACKWARD; + + _frameTime = time; + _eof = false; + _frameTexValid = false; + + if (av_seek_frame(_formatContext, _streamIndex, + llround(time / av_q2d(_stream->time_base)), seekFlags) < 0) + { + logger.error("av_seek_frame() failed", "TVideoPlayback_ffmpeg.SetPosition"); + return; + } + + avcodec_flush_buffers(_codecContext); +} + +double FFmpegVideoDecodeStream::getPosition() { + return _frameTime; +} + +int FFmpegVideoDecodeStream::getFrameWidth() { + return _codecContext->width; +} + +int FFmpegVideoDecodeStream::getFrameHeight() { + return _codecContext->height; +} + +double FFmpegVideoDecodeStream::getFrameAspect() { + return _aspect; +} + +/************************************ + * C Interface + ************************************/ + +#define VideoDecodeStreamObj(ptr) reinterpret_cast(ptr) + +static BOOL PLUGIN_CALL ffmpegVideoDecoder_init() { + return TRUE; +} + +static BOOL PLUGIN_CALL ffmpegVideoDecoder_finalize() { + return TRUE; +} + +static videoDecodeStream_t* PLUGIN_CALL ffmpegVideoDecoder_open(const char *filename) { + return (videoDecodeStream_t*)FFmpegVideoDecodeStream::open(filename); +} + +static void PLUGIN_CALL ffmpegVideoDecoder_close(videoDecodeStream_t *stream) { + delete VideoDecodeStreamObj(stream); +} + +static void PLUGIN_CALL ffmpegVideoDecoder_setLoop(videoDecodeStream_t *stream, BOOL enable) { + VideoDecodeStreamObj(stream)->setLoop(enable); +} + +static BOOL PLUGIN_CALL ffmpegVideoDecoder_getLoop(videoDecodeStream_t *stream) { + return (BOOL)VideoDecodeStreamObj(stream)->getLoop(); +} + +static void PLUGIN_CALL ffmpegVideoDecoder_setPosition(videoDecodeStream_t *stream, double time) { + VideoDecodeStreamObj(stream)->setPosition(time); +} + +static double PLUGIN_CALL ffmpegVideoDecoder_getPosition(videoDecodeStream_t *stream) { + return VideoDecodeStreamObj(stream)->getPosition(); +} + +static int PLUGIN_CALL ffmpegVideoDecoder_getFrameWidth(videoDecodeStream_t *stream) { + return VideoDecodeStreamObj(stream)->getFrameWidth(); +} + +static int PLUGIN_CALL ffmpegVideoDecoder_getFrameHeight(videoDecodeStream_t *stream) { + return VideoDecodeStreamObj(stream)->getFrameHeight(); +} + +static double PLUGIN_CALL ffmpegVideoDecoder_getFrameAspect(videoDecodeStream_t *stream) { + return VideoDecodeStreamObj(stream)->getFrameAspect(); +} + +static uint8_t* PLUGIN_CALL ffmpegVideoDecoder_getFrame(videoDecodeStream_t *stream, long double time) { + return VideoDecodeStreamObj(stream)->getFrame(time); +} + +/************************************ + * Module information + ************************************/ + +const videoDecoderInfo_t videoDecoderInfo = { + 80, + ffmpegVideoDecoder_init, + ffmpegVideoDecoder_finalize, + ffmpegVideoDecoder_open, + ffmpegVideoDecoder_close, + ffmpegVideoDecoder_setLoop, + ffmpegVideoDecoder_getLoop, + ffmpegVideoDecoder_setPosition, + ffmpegVideoDecoder_getPosition, + ffmpegVideoDecoder_getFrameWidth, + ffmpegVideoDecoder_getFrameHeight, + ffmpegVideoDecoder_getFrameAspect, + ffmpegVideoDecoder_getFrame +}; -- cgit v1.2.3