open_toontown_panda3d/panda/src/ffmpeg/ffmpegVideoCursor.cxx

1299 lines
42 KiB
C++

// Filename: ffmpegVideoCursor.cxx
// Created by: jyelon (01Aug2007)
//
////////////////////////////////////////////////////////////////////
//
// PANDA 3D SOFTWARE
// Copyright (c) Carnegie Mellon University. All rights reserved.
//
// All use of this software is subject to the terms of the revised BSD
// license. You should have received a copy of this license along
// with this source code in a file named "LICENSE."
//
////////////////////////////////////////////////////////////////////
#include "ffmpegVideoCursor.h"
#include "config_movies.h"
#include "pStatCollector.h"
#include "pStatTimer.h"
#include "mutexHolder.h"
#include "reMutexHolder.h"
#include "ffmpegVideo.h"
#include "bamReader.h"
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#ifdef HAVE_SWSCALE
#include "libswscale/swscale.h"
#endif
}
ReMutex FfmpegVideoCursor::_av_lock;
TypeHandle FfmpegVideoCursor::_type_handle;
TypeHandle FfmpegVideoCursor::FfmpegBuffer::_type_handle;
PStatCollector FfmpegVideoCursor::_fetch_buffer_pcollector("*:FFMPEG Video Decoding:Fetch");
PStatCollector FfmpegVideoCursor::_seek_pcollector("*:FFMPEG Video Decoding:Seek");
PStatCollector FfmpegVideoCursor::_export_frame_pcollector("*:FFMPEG Convert Video to BGR");
#if LIBAVFORMAT_VERSION_MAJOR < 53
#define AVMEDIA_TYPE_VIDEO CODEC_TYPE_VIDEO
#endif
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::Default Constructor
// Access: Private
// Description: This constructor is only used when reading from a bam
// file.
////////////////////////////////////////////////////////////////////
FfmpegVideoCursor::
FfmpegVideoCursor() :
_max_readahead_frames(0),
_thread_priority(ffmpeg_thread_priority),
_lock("FfmpegVideoCursor::_lock"),
_action_cvar(_lock),
_thread_status(TS_stopped),
_seek_frame(0),
_packet(NULL),
_format_ctx(NULL),
_video_ctx(NULL),
_convert_ctx(NULL),
_video_index(-1),
_frame(NULL),
_frame_out(NULL),
_eof_known(false)
{
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::init_from
// Access: Private
// Description: Specifies the source of the video cursor. This is
// normally called only by the constructor or when
// reading from a bam file.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
init_from(FfmpegVideo *source) {
nassertv(_thread == NULL && _thread_status == TS_stopped);
nassertv(source != NULL);
_source = source;
_filename = _source->get_filename();
if (!open_stream()) {
cleanup();
return;
}
ReMutexHolder av_holder(_av_lock);
#ifdef HAVE_SWSCALE
nassertv(_convert_ctx == NULL);
_convert_ctx = sws_getContext(_size_x, _size_y,
_video_ctx->pix_fmt, _size_x, _size_y,
PIX_FMT_BGR24, SWS_BILINEAR | SWS_PRINT_INFO, NULL, NULL, NULL);
#endif // HAVE_SWSCALE
_frame = avcodec_alloc_frame();
_frame_out = avcodec_alloc_frame();
if ((_frame == 0)||(_frame_out == 0)) {
cleanup();
return;
}
_packet = new AVPacket;
memset(_packet, 0, sizeof(AVPacket));
fetch_packet(0);
fetch_frame(-1);
_initial_dts = _begin_frame;
_current_frame = -1;
_eof_known = false;
_eof_frame = 0;
#ifdef HAVE_THREADS
set_max_readahead_frames(ffmpeg_max_readahead_frames);
#endif // HAVE_THREADS
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::Constructor
// Access: Published
// Description:
////////////////////////////////////////////////////////////////////
FfmpegVideoCursor::
FfmpegVideoCursor(FfmpegVideo *src) :
_max_readahead_frames(0),
_thread_priority(ffmpeg_thread_priority),
_lock("FfmpegVideoCursor::_lock"),
_action_cvar(_lock),
_thread_status(TS_stopped),
_seek_frame(0),
_packet(NULL),
_format_ctx(NULL),
_video_ctx(NULL),
_convert_ctx(NULL),
_video_index(-1),
_frame(NULL),
_frame_out(NULL),
_eof_known(false)
{
init_from(src);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::Destructor
// Access: Published
// Description:
////////////////////////////////////////////////////////////////////
FfmpegVideoCursor::
~FfmpegVideoCursor() {
cleanup();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::set_max_readahead_frames
// Access: Published
// Description: Specifies the maximum number of frames that a
// sub-thread will attempt to read ahead of the current
// frame. Setting this to a nonzero allows the video
// decoding to take place in a sub-thread, which
// smoothes out the video decoding time by spreading it
// evenly over several frames. Set this number larger
// to increase the buffer between the currently visible
// frame and the first undecoded frame; set it smaller
// to reduce memory consumption.
//
// Setting this to zero forces the video to be decoded
// in the main thread. If threading is not available in
// the Panda build, this value is always zero.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
set_max_readahead_frames(int max_readahead_frames) {
#ifndef HAVE_THREADS
if (max_readahead_frames > 0) {
ffmpeg_cat.warning()
<< "Couldn't set max_readahead_frames to " << max_readahead_frames
<< ": threading not available.\n";
max_readahead_frames = 0;
}
#endif // HAVE_THREADS
_max_readahead_frames = max_readahead_frames;
if (_max_readahead_frames > 0) {
if (_thread_status == TS_stopped) {
start_thread();
}
} else {
if (_thread_status != TS_stopped) {
stop_thread();
}
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::get_max_readahead_frames
// Access: Published
// Description: Returns the maximum number of frames that a
// sub-thread will attempt to read ahead of the current
// frame. See set_max_readahead_frames().
////////////////////////////////////////////////////////////////////
int FfmpegVideoCursor::
get_max_readahead_frames() const {
return _max_readahead_frames;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::set_thread_priority
// Access: Published
// Description: Changes the thread priority of the thread that
// decodes the ffmpeg video stream (if
// max_readahead_frames is nonzero). Normally you
// shouldn't mess with this, but there may be special
// cases where a precise balance of CPU utilization
// between the main thread and the various ffmpeg
// service threads may be needed.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
set_thread_priority(ThreadPriority thread_priority) {
if (_thread_priority != thread_priority) {
_thread_priority = thread_priority;
if (is_thread_started()) {
stop_thread();
start_thread();
}
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::get_thread_priority
// Access: Published
// Description: Returns the current thread priority of the thread that
// decodes the ffmpeg video stream (if
// max_readahead_frames is nonzero). See
// set_thread_priority().
////////////////////////////////////////////////////////////////////
ThreadPriority FfmpegVideoCursor::
get_thread_priority() const {
return _thread_priority;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::start_thread
// Access: Published
// Description: Explicitly starts the ffmpeg decoding thread after it
// has been stopped by a call to stop_thread(). The
// thread is normally started automatically, so there is
// no need to call this method unless you have
// previously called stop_thread() for some reason.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
start_thread() {
MutexHolder holder(_lock);
if (_thread_status == TS_stopped && _max_readahead_frames > 0) {
// Get a unique name for the thread's sync name.
ostringstream strm;
strm << (void *)this;
_sync_name = strm.str();
// Create and start the thread object.
_thread_status = TS_wait;
_thread = new GenericThread(_filename.get_basename(), _sync_name, st_thread_main, this);
if (!_thread->start(_thread_priority, true)) {
// Couldn't start the thread.
_thread = NULL;
_thread_status = TS_stopped;
}
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::stop_thread
// Access: Published
// Description: Explicitly stops the ffmpeg decoding thread. There
// is normally no reason to do this unless you want to
// maintain precise control over what threads are
// consuming CPU resources. Calling this method will
// make the video update in the main thread, regardless
// of the setting of max_readahead_frames, until you
// call start_thread() again.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
stop_thread() {
if (_thread_status != TS_stopped) {
PT(GenericThread) thread = _thread;
{
MutexHolder holder(_lock);
if (_thread_status != TS_stopped) {
_thread_status = TS_shutdown;
}
_action_cvar.notify();
_thread = NULL;
}
// Now that we've released the lock, we can join the thread.
thread->join();
}
// This is a good time to clean up all of the allocated frame
// objects. It's not really necessary to be holding the lock, since
// the thread is gone, but we'll grab it anyway just in case someone
// else starts the thread up again.
MutexHolder holder(_lock);
_readahead_frames.clear();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::is_thread_started
// Access: Published
// Description: Returns true if the thread has been started, false if
// not. This will always return false if
// max_readahead_frames is 0.
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
is_thread_started() const {
return (_thread_status != TS_stopped);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::set_time
// Access: Published, Virtual
// Description: See MovieVideoCursor::set_time().
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
set_time(double timestamp, int loop_count) {
int frame = (int)(timestamp / _video_timebase + 0.5);
if (_eof_known) {
if (loop_count == 0) {
frame = frame % _eof_frame;
} else {
int last_frame = _eof_frame * loop_count;
if (frame < last_frame) {
frame = frame % _eof_frame;
} else {
frame = _eof_frame - 1;
}
}
}
// No point in trying to position before the first frame.
frame = max(frame, _initial_dts);
if (ffmpeg_cat.is_spam() && frame != _current_frame) {
ffmpeg_cat.spam()
<< "set_time(" << timestamp << "): " << frame
<< ", loop_count = " << loop_count << "\n";
}
_current_frame = frame;
if (_current_frame_buffer != NULL) {
// If we've previously returned a frame, don't bother asking for a
// next one if that frame is still valid.
return (_current_frame >= _current_frame_buffer->_end_frame ||
_current_frame < _current_frame_buffer->_begin_frame);
}
// If our last request didn't return a frame, try again.
return true;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::fetch_buffer
// Access: Public, Virtual
// Description: See MovieVideoCursor::fetch_buffer.
////////////////////////////////////////////////////////////////////
PT(MovieVideoCursor::Buffer) FfmpegVideoCursor::
fetch_buffer() {
MutexHolder holder(_lock);
// If there was an error at any point, just return NULL.
if (_format_ctx == (AVFormatContext *)NULL) {
return NULL;
}
PT(FfmpegBuffer) frame;
if (_thread_status == TS_stopped) {
// Non-threaded case. Just get the next frame directly.
advance_to_frame(_current_frame);
if (_frame_ready) {
frame = do_alloc_frame();
export_frame(frame);
}
} else {
// Threaded case. Wait for the thread to serve up the required
// frames.
if (!_readahead_frames.empty()) {
frame = _readahead_frames.front();
_readahead_frames.pop_front();
_action_cvar.notify();
while (frame->_end_frame < _current_frame && !_readahead_frames.empty()) {
// This frame is too old. Discard it.
if (ffmpeg_cat.is_debug()) {
ffmpeg_cat.debug()
<< "ffmpeg for " << _filename.get_basename()
<< " at frame " << _current_frame << ", discarding frame at "
<< frame->_begin_frame << "\n";
}
frame = _readahead_frames.front();
_readahead_frames.pop_front();
}
if (frame->_begin_frame > _current_frame) {
// This frame is too new. Empty all remaining frames and seek
// backwards.
if (ffmpeg_cat.is_debug()) {
ffmpeg_cat.debug()
<< "ffmpeg for " << _filename.get_basename()
<< " at frame " << _current_frame << ", encountered too-new frame at "
<< frame->_begin_frame << "\n";
}
do_clear_all_frames();
if (_thread_status == TS_wait || _thread_status == TS_seek || _thread_status == TS_readahead) {
_thread_status = TS_seek;
_seek_frame = _current_frame;
_action_cvar.notify();
}
}
}
if (frame == NULL || frame->_end_frame < _current_frame) {
// No frame available, or the frame is too old. Seek.
if (_thread_status == TS_wait || _thread_status == TS_seek || _thread_status == TS_readahead) {
_thread_status = TS_seek;
_seek_frame = _current_frame;
_action_cvar.notify();
}
}
}
if (frame != NULL) {
bool too_old = (frame->_end_frame < _current_frame && !ffmpeg_show_seek_frames);
bool too_new = frame->_begin_frame > _current_frame;
if (too_old || too_new) {
// The frame is too old or too new. Just recycle it.
frame = NULL;
}
}
if (frame != NULL) {
_current_frame_buffer = frame;
if (ffmpeg_cat.is_debug()) {
ffmpeg_cat.debug()
<< "ffmpeg for " << _filename.get_basename()
<< " at frame " << _current_frame << ", returning frame at "
<< frame->_begin_frame << "\n";
}
} else {
if (ffmpeg_cat.is_debug()) {
ffmpeg_cat.debug()
<< "ffmpeg for " << _filename.get_basename()
<< " at frame " << _current_frame << ", returning NULL\n";
}
}
return frame.p();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::make_new_buffer
// Access: Protected, Virtual
// Description: May be called by a derived class to allocate a new
// Buffer object.
////////////////////////////////////////////////////////////////////
PT(MovieVideoCursor::Buffer) FfmpegVideoCursor::
make_new_buffer() {
PT(FfmpegBuffer) frame = new FfmpegBuffer(size_x() * size_y() * get_num_components(), _video_timebase);
return frame.p();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::open_stream
// Access: Private
// Description: Opens the stream for the first time, or when needed
// internally.
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
open_stream() {
nassertr(!_ffvfile.is_open(), false);
// Hold the global lock while we open the file and create avcodec
// objects.
ReMutexHolder av_holder(_av_lock);
if (!_source->get_subfile_info().is_empty()) {
// Read a subfile.
if (!_ffvfile.open_subfile(_source->get_subfile_info())) {
ffmpeg_cat.info()
<< "Couldn't open " << _source->get_subfile_info() << "\n";
close_stream();
return false;
}
} else {
// Read a filename.
if (!_ffvfile.open_vfs(_filename)) {
ffmpeg_cat.info()
<< "Couldn't open " << _filename << "\n";
close_stream();
return false;
}
}
nassertr(_format_ctx == NULL, false);
_format_ctx = _ffvfile.get_format_context();
nassertr(_format_ctx != NULL, false);
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(53, 6, 0)
if (avformat_find_stream_info(_format_ctx, NULL) < 0) {
#else
if (av_find_stream_info(_format_ctx) < 0) {
#endif
ffmpeg_cat.info()
<< "Couldn't find stream info\n";
close_stream();
return false;
}
// Find the video stream
nassertr(_video_ctx == NULL, false);
for (int i = 0; i < (int)_format_ctx->nb_streams; ++i) {
if (_format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
_video_index = i;
_video_ctx = _format_ctx->streams[i]->codec;
_video_timebase = av_q2d(_format_ctx->streams[i]->time_base);
_min_fseek = (int)(3.0 / _video_timebase);
}
}
if (_video_ctx == NULL) {
ffmpeg_cat.info()
<< "Couldn't find video_ctx\n";
close_stream();
return false;
}
AVCodec *pVideoCodec = avcodec_find_decoder(_video_ctx->codec_id);
if (pVideoCodec == NULL) {
ffmpeg_cat.info()
<< "Couldn't find codec\n";
close_stream();
return false;
}
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(53, 8, 0)
if (avcodec_open2(_video_ctx, pVideoCodec, NULL) < 0) {
#else
if (avcodec_open(_video_ctx, pVideoCodec) < 0) {
#endif
ffmpeg_cat.info()
<< "Couldn't open codec\n";
close_stream();
return false;
}
_size_x = _video_ctx->width;
_size_y = _video_ctx->height;
_num_components = 3; // Don't know how to implement RGBA movies yet.
_length = (double)_format_ctx->duration / (double)AV_TIME_BASE;
_can_seek = true;
_can_seek_fast = true;
return true;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::close_stream
// Access: Private
// Description: Closes the stream, during cleanup or when needed
// internally.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
close_stream() {
// Hold the global lock while we free avcodec objects.
ReMutexHolder av_holder(_av_lock);
if ((_video_ctx)&&(_video_ctx->codec)) {
avcodec_close(_video_ctx);
}
_video_ctx = NULL;
_ffvfile.close();
_format_ctx = NULL;
_video_index = -1;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::cleanup
// Access: Private
// Description: Reset to a standard inactive state.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
cleanup() {
stop_thread();
close_stream();
ReMutexHolder av_holder(_av_lock);
#ifdef HAVE_SWSCALE
if (_convert_ctx != NULL) {
sws_freeContext(_convert_ctx);
}
_convert_ctx = NULL;
#endif // HAVE_SWSCALE
if (_frame) {
av_free(_frame);
_frame = NULL;
}
if (_frame_out) {
_frame_out->data[0] = 0;
av_free(_frame_out);
_frame_out = NULL;
}
if (_packet) {
if (_packet->data) {
av_free_packet(_packet);
}
delete _packet;
_packet = NULL;
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::st_thread_main
// Access: Private, Static
// Description: The thread main function, static version (for passing
// to GenericThread).
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
st_thread_main(void *self) {
((FfmpegVideoCursor *)self)->thread_main();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::thread_main
// Access: Private
// Description: The thread main function.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
thread_main() {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "ffmpeg thread for " << _filename.get_basename() << " starting.\n";
}
// First, push the first frame onto the readahead queue.
if (_frame_ready) {
PT(FfmpegBuffer) frame = do_alloc_frame();
export_frame(frame);
MutexHolder holder(_lock);
_readahead_frames.push_back(frame);
}
// Now repeatedly wait for something interesting to do, until we're told
// to shut down.
MutexHolder holder(_lock);
while (_thread_status != TS_shutdown) {
nassertv(_thread_status != TS_stopped);
_action_cvar.wait();
while (do_poll()) {
// Keep doing stuff as long as there's something to do.
_lock.release();
PStatClient::thread_tick(_sync_name);
Thread::consider_yield();
_lock.acquire();
}
}
_thread_status = TS_stopped;
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "ffmpeg thread for " << _filename.get_basename() << " stopped.\n";
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_poll
// Access: Private
// Description: Called within the sub-thread. Assumes the lock is
// already held. If there is something for the thread
// to do, does it and returns true. If there is nothing
// for the thread to do, returns false.
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
do_poll() {
switch (_thread_status) {
case TS_stopped:
case TS_seeking:
// This shouldn't be possible while the thread is running.
nassertr(false, false);
return false;
case TS_wait:
// The video hasn't started playing yet.
return false;
case TS_readahead:
if ((int)_readahead_frames.size() < _max_readahead_frames) {
// Time to read the next frame.
PT(FfmpegBuffer) frame = do_alloc_frame();
nassertr(frame != NULL, false);
_lock.release();
fetch_frame(-1);
if (_frame_ready) {
export_frame(frame);
_lock.acquire();
_readahead_frames.push_back(frame);
} else {
// No frame.
_lock.acquire();
}
return true;
}
// No room for the next frame yet. Wait for more.
return false;
case TS_seek:
// Seek to a specific frame.
{
int seek_frame = _seek_frame;
_thread_status = TS_seeking;
PT(FfmpegBuffer) frame = do_alloc_frame();
nassertr(frame != NULL, false);
_lock.release();
advance_to_frame(seek_frame);
if (_frame_ready) {
export_frame(frame);
_lock.acquire();
do_clear_all_frames();
_readahead_frames.push_back(frame);
} else {
_lock.acquire();
do_clear_all_frames();
}
if (_thread_status == TS_seeking) {
// After seeking, we automatically transition to readahead.
_thread_status = TS_readahead;
}
}
return true;
case TS_shutdown:
// Time to stop the thread.
return false;
}
return false;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_alloc_frame
// Access: Private
// Description: Allocates a new Buffer object. Assumes the lock is
// held.
////////////////////////////////////////////////////////////////////
PT(FfmpegVideoCursor::FfmpegBuffer) FfmpegVideoCursor::
do_alloc_frame() {
PT(Buffer) buffer = make_new_buffer();
return (FfmpegBuffer *)buffer.p();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_clear_all_frames
// Access: Private
// Description: Empties the entire readahead_frames queue.
// Assumes the lock is held.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
do_clear_all_frames() {
_readahead_frames.clear();
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::fetch_packet
// Access: Private
// Description: Called within the sub-thread. Fetches a video packet
// and stores it in the packet0 buffer. Sets packet_frame
// to the packet's timestamp. If a packet could not be
// read, the packet is cleared and the packet_frame is
// set to the specified default value. Returns true on
// failure (such as the end of the video), or false on
// success.
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
fetch_packet(int default_frame) {
if (ffmpeg_global_lock) {
ReMutexHolder av_holder(_av_lock);
return do_fetch_packet(default_frame);
} else {
return do_fetch_packet(default_frame);
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_fetch_packet
// Access: Private
// Description: As above, with the ffmpeg global lock held (if
// configured on).
////////////////////////////////////////////////////////////////////
bool FfmpegVideoCursor::
do_fetch_packet(int default_frame) {
if (_packet->data) {
av_free_packet(_packet);
}
while (av_read_frame(_format_ctx, _packet) >= 0) {
if (_packet->stream_index == _video_index) {
_packet_frame = _packet->dts;
return false;
}
av_free_packet(_packet);
}
_packet->data = 0;
if (!_eof_known && default_frame != 0) {
_eof_frame = _packet_frame;
_eof_known = true;
}
if (ffmpeg_cat.is_spam()) {
if (_eof_known) {
ffmpeg_cat.spam()
<< "end of video at frame " << _eof_frame << "\n";
} else {
ffmpeg_cat.spam()
<< "end of video\n";
}
}
_packet_frame = default_frame;
return true;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::fetch_frame
// Access: Private
// Description: Called within the sub-thread. Slides forward until
// the indicated frame, then fetches a frame from the
// stream and stores it in the frame buffer. Sets
// _begin_frame and _end_frame to indicate the extents of
// the frame. Sets _frame_ready true to indicate a
// frame is now available, or false if it is not (for
// instance, because the end of the video was reached).
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
fetch_frame(int frame) {
PStatTimer timer(_fetch_buffer_pcollector);
int finished = 0;
if (_packet_frame <= frame) {
finished = 0;
// Get the next packet. The first packet beyond the frame we're
// looking for marks the point to stop.
while (_packet_frame <= frame) {
PStatTimer timer(_seek_pcollector);
// Decode the previous packet, and get the next one.
decode_frame(finished);
_begin_frame = _packet_frame;
if (fetch_packet(frame)) {
_end_frame = _packet_frame;
_frame_ready = false;
return;
}
}
} else {
// Just get the next frame.
finished = 0;
while (!finished && _packet->data) {
decode_frame(finished);
_begin_frame = _packet_frame;
fetch_packet(_begin_frame + 1);
}
}
_end_frame = _packet_frame;
_frame_ready = true;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::decode_frame
// Access: Private
// Description: Called within the sub-thread. Decodes the data in
// the specified packet into _frame.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
decode_frame(int &finished) {
if (ffmpeg_global_lock) {
ReMutexHolder av_holder(_av_lock);
do_decode_frame(finished);
} else {
do_decode_frame(finished);
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_decode_frame
// Access: Private
// Description: As above, with the ffmpeg global lock held (if
// configured on).
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
do_decode_frame(int &finished) {
#if LIBAVCODEC_VERSION_INT < 3414272
avcodec_decode_video(_video_ctx, _frame,
&finished, _packet->data, _packet->size);
#else
avcodec_decode_video2(_video_ctx, _frame, &finished, _packet);
#endif
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::seek
// Access: Private
// Description: Called within the sub-thread. Seeks to a target
// location. Afterward, the packet_frame is guaranteed
// to be less than or equal to the specified frame.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
seek(int frame, bool backward) {
PStatTimer timer(_seek_pcollector);
if (ffmpeg_support_seek) {
if (ffmpeg_global_lock) {
ReMutexHolder av_holder(_av_lock);
do_seek(frame, backward);
} else {
do_seek(frame, backward);
}
} else {
// If seeking isn't supported, close-and-reopen.
if (backward) {
reset_stream();
}
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::do_seek
// Access: Private
// Description: As above, with the ffmpeg global lock held (if
// configured on). Also only if ffmpeg-support-seek is
// on.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
do_seek(int frame, bool backward) {
PN_int64 target_ts = (PN_int64)frame;
if (target_ts < (PN_int64)(_initial_dts)) {
// Attempts to seek before the first packet will fail.
target_ts = _initial_dts;
}
int flags = 0;
if (backward) {
flags = AVSEEK_FLAG_BACKWARD;
}
if (av_seek_frame(_format_ctx, _video_index, target_ts, flags) < 0) {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Seek failure.\n";
}
if (backward) {
// Now try to seek forward.
reset_stream();
seek(frame, false);
return;
}
// Try a binary search to get a little closer.
if (binary_seek(_initial_dts, frame, frame, 1) < 0) {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Seek double failure.\n";
}
reset_stream();
return;
}
}
fetch_packet(0);
fetch_frame(-1);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::binary_seek
// Access: Private
// Description: Casts about within the stream for a reasonably-close
// frame to seek to. We're trying to get as close as
// possible to target_frame.
////////////////////////////////////////////////////////////////////
int FfmpegVideoCursor::
binary_seek(int min_frame, int max_frame, int target_frame, int num_iterations) {
int try_frame = (min_frame + max_frame) / 2;
if (num_iterations > 5 || try_frame >= max_frame) {
// Success.
return 0;
}
if (av_seek_frame(_format_ctx, _video_index, try_frame, AVSEEK_FLAG_BACKWARD) < 0) {
// Failure. Try lower.
if (binary_seek(min_frame, try_frame - 1, target_frame, num_iterations + 1) < 0) {
return -1;
}
} else {
// Success. Try higher.
if (binary_seek(try_frame + 1, max_frame, target_frame, num_iterations + 1) < 0) {
return -1;
}
}
return 0;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::reset_stream
// Access: Private
// Description: Resets the stream to its initial, first-opened state
// by closing and re-opening it.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
reset_stream() {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Resetting ffmpeg stream.\n";
}
close_stream();
if (!open_stream()) {
ffmpeg_cat.error()
<< "Stream error, invalidating movie.\n";
cleanup();
return;
}
fetch_packet(0);
fetch_frame(-1);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::advance_to_frame
// Access: Private
// Description: Called within the sub-thread. Advance until the
// specified frame is in the export buffer.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
advance_to_frame(int frame) {
PStatTimer timer(_fetch_buffer_pcollector);
if (frame < _begin_frame) {
// Frame is in the past.
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Seeking backward to " << frame << " from " << _begin_frame << "\n";
}
seek(frame, true);
if (_begin_frame > frame) {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Ended up at " << _begin_frame << ", not far enough back!\n";
}
reset_stream();
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Reseek to 0, got " << _begin_frame << "\n";
}
}
if (frame > _end_frame) {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Now sliding forward to " << frame << " from " << _begin_frame << "\n";
}
fetch_frame(frame);
}
} else if (frame < _end_frame) {
// Frame is in the present: already have the frame.
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Currently have " << frame << " within " << _begin_frame << " .. " << _end_frame << "\n";
}
} else if (frame < _end_frame + _min_fseek) {
// Frame is in the near future.
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Sliding forward to " << frame << " from " << _begin_frame << "\n";
}
fetch_frame(frame);
} else {
// Frame is in the far future. Seek forward, then read.
// There's a danger here: because keyframes are spaced
// unpredictably, trying to seek forward could actually
// move us backward in the stream! This must be avoided.
// So the rule is, try the seek. If it hurts us by moving
// us backward, we increase the minimum threshold distance
// for forward-seeking in the future.
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Jumping forward to " << frame << " from " << _begin_frame << "\n";
}
int base = _begin_frame;
seek(frame, false);
if (_begin_frame < base) {
_min_fseek += (base - _begin_frame);
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Wrong way! Increasing _min_fseek to " << _min_fseek << "\n";
}
}
if (frame > _end_frame) {
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Correcting, sliding forward to " << frame << " from " << _begin_frame << "\n";
}
fetch_frame(frame);
}
}
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "Wanted " << frame << ", got " << _begin_frame << "\n";
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::export_frame
// Access: Private
// Description: Called within the sub-thread. Exports the contents
// of the frame buffer into the indicated target buffer.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
export_frame(FfmpegBuffer *buffer) {
PStatTimer timer(_export_frame_pcollector);
if (!_frame_ready) {
// No frame data ready, just fill with black.
if (ffmpeg_cat.is_spam()) {
ffmpeg_cat.spam()
<< "ffmpeg for " << _filename.get_basename()
<< ", no frame available.\n";
}
memset(buffer->_block, 0, buffer->_block_size);
return;
}
_frame_out->data[0] = buffer->_block + ((_size_y - 1) * _size_x * 3);
_frame_out->linesize[0] = _size_x * -3;
buffer->_begin_frame = _begin_frame;
buffer->_end_frame = _end_frame;
if (ffmpeg_global_lock) {
ReMutexHolder av_holder(_av_lock);
#ifdef HAVE_SWSCALE
nassertv(_convert_ctx != NULL && _frame != NULL && _frame_out != NULL);
sws_scale(_convert_ctx, _frame->data, _frame->linesize, 0, _size_y, _frame_out->data, _frame_out->linesize);
#else
img_convert((AVPicture *)_frame_out, PIX_FMT_BGR24,
(AVPicture *)_frame, _video_ctx->pix_fmt, _size_x, _size_y);
#endif
} else {
#ifdef HAVE_SWSCALE
nassertv(_convert_ctx != NULL && _frame != NULL && _frame_out != NULL);
sws_scale(_convert_ctx, _frame->data, _frame->linesize, 0, _size_y, _frame_out->data, _frame_out->linesize);
#else
img_convert((AVPicture *)_frame_out, PIX_FMT_BGR24,
(AVPicture *)_frame, _video_ctx->pix_fmt, _size_x, _size_y);
#endif
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::register_with_read_factory
// Access: Public, Static
// Description: Tells the BamReader how to create objects of type
// FfmpegVideo.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
register_with_read_factory() {
BamReader::get_factory()->register_factory(get_class_type(), make_from_bam);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::write_datagram
// Access: Public, Virtual
// Description: Writes the contents of this object to the datagram
// for shipping out to a Bam file.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
write_datagram(BamWriter *manager, Datagram &dg) {
MovieVideoCursor::write_datagram(manager, dg);
// No need to write any additional data here--all of it comes
// implicitly from the underlying MovieVideo, which we process in
// finalize().
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::finalize
// Access: Public, Virtual
// Description: Called by the BamReader to perform any final actions
// needed for setting up the object after all objects
// have been read and all pointers have been completed.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
finalize(BamReader *) {
if (_source != (MovieVideo *)NULL) {
FfmpegVideo *video;
DCAST_INTO_V(video, _source);
init_from(video);
}
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::make_from_bam
// Access: Private, Static
// Description: This function is called by the BamReader's factory
// when a new object of type FfmpegVideo is encountered
// in the Bam file. It should create the FfmpegVideo
// and extract its information from the file.
////////////////////////////////////////////////////////////////////
TypedWritable *FfmpegVideoCursor::
make_from_bam(const FactoryParams &params) {
FfmpegVideoCursor *video = new FfmpegVideoCursor;
DatagramIterator scan;
BamReader *manager;
parse_params(params, scan, manager);
video->fillin(scan, manager);
return video;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::fillin
// Access: Private
// Description: This internal function is called by make_from_bam to
// read in all of the relevant data from the BamFile for
// the new FfmpegVideo.
////////////////////////////////////////////////////////////////////
void FfmpegVideoCursor::
fillin(DatagramIterator &scan, BamReader *manager) {
MovieVideoCursor::fillin(scan, manager);
// The MovieVideoCursor gets the underlying MovieVideo pointer. We
// need a finalize callback so we can initialize ourselves once that
// has been read completely.
manager->register_finalize(this);
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::FfmpegBuffer::compare_timestamp
// Access: Published, Virtual
// Description: Used to sort different buffers to ensure they
// correspond to the same source frame, particularly
// important when synchronizing the different pages of a
// multi-page texture.
//
// Returns 0 if the two buffers are of the same frame,
// <0 if this one comes earlier than the other one, and
// >0 if the other one comes earlier.
////////////////////////////////////////////////////////////////////
int FfmpegVideoCursor::FfmpegBuffer::
compare_timestamp(const Buffer *other) const {
const FfmpegBuffer *fother;
DCAST_INTO_R(fother, other, 0);
if (_end_frame * _video_timebase <= fother->_begin_frame * fother->_video_timebase) {
return -1;
} else if (_begin_frame * _video_timebase >= fother->_end_frame * fother->_video_timebase) {
return 1;
}
return 0;
}
////////////////////////////////////////////////////////////////////
// Function: FfmpegVideoCursor::FfmpegBuffer::get_timestamp
// Access: Published, Virtual
// Description: Returns the nearest timestamp value of this
// particular buffer. Ideally,
// MovieVideoCursor::set_time() for this timestamp would
// return this buffer again. This need be defined only
// if compare_timestamp() is also defined.
////////////////////////////////////////////////////////////////////
double FfmpegVideoCursor::FfmpegBuffer::
get_timestamp() const {
int mid_frame = (_begin_frame + _end_frame - 1) / 2;
return mid_frame * _video_timebase;
}