// 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"

#ifdef HAVE_FFMPEG

#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 (av_find_stream_info(_format_ctx) < 0) {
    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 (avcodec_open(_video_ctx, pVideoCodec) < 0) {
    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;
}

#endif // HAVE_FFMPEG
