// 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_ffmpeg.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 #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(54, 59, 100) _frame = av_frame_alloc(); _frame_out = av_frame_alloc(); #else _frame = avcodec_alloc_frame(); _frame_out = avcodec_alloc_frame(); #endif 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 ¶ms) { 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; }