|
|
@@ -34,15 +34,12 @@ FfmpegVideo::
|
|
|
FfmpegVideo(const Filename &name) :
|
|
|
MovieVideo(name),
|
|
|
_filename(name),
|
|
|
- _samples_read(0),
|
|
|
_format_ctx(0),
|
|
|
_video_index(-1),
|
|
|
- _audio_index(-1),
|
|
|
_video_ctx(0),
|
|
|
- _audio_ctx(0),
|
|
|
_frame(0),
|
|
|
_frame_out(0),
|
|
|
- _time_correction_next(0)
|
|
|
+ _min_fseek(3.0)
|
|
|
{
|
|
|
string osname = _filename.to_os_specific();
|
|
|
if (av_open_input_file(&_format_ctx, osname.c_str(), NULL, 0, NULL)!=0) {
|
|
|
@@ -55,18 +52,13 @@ FfmpegVideo(const Filename &name) :
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Find the video and audio streams
|
|
|
+ // Find the video stream
|
|
|
for(int i=0; i<_format_ctx->nb_streams; i++) {
|
|
|
if(_format_ctx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) {
|
|
|
_video_index = i;
|
|
|
_video_ctx = _format_ctx->streams[i]->codec;
|
|
|
_video_timebase = av_q2d(_format_ctx->streams[i]->time_base);
|
|
|
}
|
|
|
- if(_format_ctx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO) {
|
|
|
- _audio_index = i;
|
|
|
- _audio_ctx = _format_ctx->streams[i]->codec;
|
|
|
- _audio_timebase = av_q2d(_format_ctx->streams[i]->time_base);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
if (_video_ctx == 0) {
|
|
|
@@ -87,34 +79,23 @@ FfmpegVideo(const Filename &name) :
|
|
|
_size_x = _video_ctx->width;
|
|
|
_size_y = _video_ctx->height;
|
|
|
_num_components = 3; // Don't know how to implement RGBA movies yet.
|
|
|
-
|
|
|
- if (_audio_ctx) {
|
|
|
- AVCodec *pAudioCodec=avcodec_find_decoder(_audio_ctx->codec_id);
|
|
|
- if (pAudioCodec == 0) {
|
|
|
- _audio_ctx = 0;
|
|
|
- _audio_index = -1;
|
|
|
- } else {
|
|
|
- if (avcodec_open(_audio_ctx, pAudioCodec)<0) {
|
|
|
- _audio_ctx = 0;
|
|
|
- _audio_index = -1;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
_length = (_format_ctx->duration * 1.0) / AV_TIME_BASE;
|
|
|
_can_seek = true;
|
|
|
_can_seek_fast = true;
|
|
|
|
|
|
- memset(_time_corrections, 0, sizeof(_time_corrections));
|
|
|
-
|
|
|
+ _packet = new AVPacket;
|
|
|
_frame = avcodec_alloc_frame();
|
|
|
_frame_out = avcodec_alloc_frame();
|
|
|
- if ((_frame == 0)||(_frame_out == 0)) {
|
|
|
+ if ((_packet == 0)||(_frame == 0)||(_frame_out == 0)) {
|
|
|
cleanup();
|
|
|
return;
|
|
|
}
|
|
|
+ memset(_packet, 0, sizeof(AVPacket));
|
|
|
|
|
|
- read_ahead();
|
|
|
+ fetch_packet(0.0);
|
|
|
+ _initial_dts = _packet->dts;
|
|
|
+ _packet_time = 0.0;
|
|
|
+ _last_start = -1.0;
|
|
|
_next_start = 0.0;
|
|
|
}
|
|
|
|
|
|
@@ -136,6 +117,13 @@ FfmpegVideo::
|
|
|
void FfmpegVideo::
|
|
|
cleanup() {
|
|
|
_frame_out->data[0] = 0;
|
|
|
+ if (_packet) {
|
|
|
+ if (_packet->data) {
|
|
|
+ av_free_packet(_packet);
|
|
|
+ }
|
|
|
+ delete _packet;
|
|
|
+ _packet = 0;
|
|
|
+ }
|
|
|
if (_format_ctx) {
|
|
|
av_close_input_file(_format_ctx);
|
|
|
_format_ctx = 0;
|
|
|
@@ -149,83 +137,103 @@ cleanup() {
|
|
|
_frame_out = 0;
|
|
|
}
|
|
|
_video_ctx = 0;
|
|
|
- _audio_ctx = 0;
|
|
|
_video_index = -1;
|
|
|
- _audio_index = -1;
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
-// Function: FfmpegVideo::read_ahead
|
|
|
-// Access: Protected
|
|
|
-// Description: The video prefetches the next video frame in order
|
|
|
-// to be able to implement the next_start query.
|
|
|
-// This function skips over audio packets, but in the
|
|
|
-// process, it updates the time_correction value.
|
|
|
-// If the stream is out of video packets, the packet
|
|
|
-// is empty.
|
|
|
+// Function: FfmpegVideo::export_frame
|
|
|
+// Access: Public, Virtual
|
|
|
+// Description: Exports the contents of the frame buffer into the
|
|
|
+// user's target buffer.
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
void FfmpegVideo::
|
|
|
-read_ahead() {
|
|
|
- if (_format_ctx == 0) {
|
|
|
- return;
|
|
|
+export_frame(unsigned char *data, bool bgra) {
|
|
|
+ if (bgra) {
|
|
|
+ _frame_out->data[0] = data + ((_size_y - 1) * _size_x * 4);
|
|
|
+ _frame_out->linesize[0] = _size_x * -4;
|
|
|
+ img_convert((AVPicture *)_frame_out, PIX_FMT_BGRA,
|
|
|
+ (AVPicture *)_frame, _video_ctx->pix_fmt, _size_x, _size_y);
|
|
|
+ } else {
|
|
|
+ _frame_out->data[0] = data + ((_size_y - 1) * _size_x * 3);
|
|
|
+ _frame_out->linesize[0] = _size_x * -3;
|
|
|
+ img_convert((AVPicture *)_frame_out, PIX_FMT_BGR24,
|
|
|
+ (AVPicture *)_frame, _video_ctx->pix_fmt, _size_x, _size_y);
|
|
|
}
|
|
|
-
|
|
|
- AVPacket pkt;
|
|
|
+}
|
|
|
|
|
|
- while(av_read_frame(_format_ctx, &pkt)>=0) {
|
|
|
-
|
|
|
- // Is this a packet from the video stream?
|
|
|
- // If so, store corrected timestamp and return.
|
|
|
-
|
|
|
- if (pkt.stream_index== _video_index) {
|
|
|
- double dts = pkt.dts * _video_timebase;
|
|
|
- double correction = get_time_correction();
|
|
|
- _next_start = dts + correction;
|
|
|
- cerr << "Video: " << dts << " " << dts+correction << "\n";
|
|
|
- if (_next_start < _last_start) {
|
|
|
- _next_start = _last_start;
|
|
|
- }
|
|
|
- int finished = 0;
|
|
|
- avcodec_decode_video(_video_ctx, _frame,
|
|
|
- &finished, pkt.data, pkt.size);
|
|
|
- if (finished) {
|
|
|
- return;
|
|
|
- }
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: FfmpegVideo::fetch_packet
|
|
|
+// Access: Protected
|
|
|
+// Description: Fetches a video packet and stores it in the
|
|
|
+// packet buffer. Sets packet_time to the packet's
|
|
|
+// timestamp. If a packet could not be read, the
|
|
|
+// packet is cleared and the packet_time is set to
|
|
|
+// the specified default value.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void FfmpegVideo::
|
|
|
+fetch_packet(double default_time) {
|
|
|
+ if (_packet->data) {
|
|
|
+ av_free_packet(_packet);
|
|
|
+ }
|
|
|
+ while (av_read_frame(_format_ctx, _packet) >= 0) {
|
|
|
+ if (_packet->stream_index == _video_index) {
|
|
|
+ _packet_time = _packet->dts * _video_timebase;
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- // Is this a packet from the audio stream?
|
|
|
- // If so, use it to calibrate the time correction value.
|
|
|
-
|
|
|
- if (pkt.stream_index== _audio_index) {
|
|
|
- double dts = pkt.dts * _audio_timebase;
|
|
|
- double real = (_samples_read * 1.0) / _audio_ctx->sample_rate;
|
|
|
- update_time_correction(real - dts);
|
|
|
+ av_free_packet(_packet);
|
|
|
+ }
|
|
|
+ _packet->data = 0;
|
|
|
+ _packet_time = default_time;
|
|
|
+}
|
|
|
|
|
|
- int16_t buffer[4096];
|
|
|
- uint8_t *audio_pkt_data = pkt.data;
|
|
|
- int audio_pkt_size = pkt.size;
|
|
|
- while(audio_pkt_size > 0) {
|
|
|
- int data_size = sizeof(buffer);
|
|
|
- int decoded = avcodec_decode_audio(_audio_ctx, buffer, &data_size,
|
|
|
- audio_pkt_data, audio_pkt_size);
|
|
|
- if(decoded <= 0) {
|
|
|
- break;
|
|
|
- }
|
|
|
- audio_pkt_data += decoded;
|
|
|
- audio_pkt_size -= decoded;
|
|
|
- if (data_size > 0) {
|
|
|
- _samples_read += data_size / (2 * _audio_ctx->channels);
|
|
|
- }
|
|
|
- }
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: FfmpegVideo::fetch_frame
|
|
|
+// Access: Protected
|
|
|
+// Description: Fetches a frame from the stream and stores it in
|
|
|
+// the frame buffer. Sets last_start and next_start
|
|
|
+// to indicate the extents of the frame.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void FfmpegVideo::
|
|
|
+fetch_frame() {
|
|
|
+ int finished = 0;
|
|
|
+ _last_start = _packet_time;
|
|
|
+ while (!finished && _packet->data) {
|
|
|
+ avcodec_decode_video(_video_ctx, _frame,
|
|
|
+ &finished, _packet->data, _packet->size);
|
|
|
+ fetch_packet(_last_start + 1.0);
|
|
|
+ }
|
|
|
+ _next_start = _packet_time;
|
|
|
+ cerr << "Fetch yields " << _last_start << " - " << _next_start << "\n";
|
|
|
+}
|
|
|
|
|
|
- cerr << "Audio: " << pkt.dts << "(" << dts << ") " << real << "\n";
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+// Function: FfmpegVideo::seek
|
|
|
+// Access: Protected
|
|
|
+// Description: Seeks to a target location. Afterward, the
|
|
|
+// packet_time is guaranteed to be less than or
|
|
|
+// equal to the specified time.
|
|
|
+////////////////////////////////////////////////////////////////////
|
|
|
+void FfmpegVideo::
|
|
|
+seek(double t) {
|
|
|
+ long long target_ts = (long long)(t / _video_timebase);
|
|
|
+ if (target_ts < (long long)(_initial_dts)) {
|
|
|
+ // Attempts to seek before the first packet will fail.
|
|
|
+ target_ts = _initial_dts;
|
|
|
+ }
|
|
|
+ if (av_seek_frame(_format_ctx, _video_index, target_ts, AVSEEK_FLAG_BACKWARD) < 0) {
|
|
|
+ if (t >= _packet_time) {
|
|
|
+ return;
|
|
|
}
|
|
|
-
|
|
|
- av_free_packet(&pkt);
|
|
|
+ movies_cat.error() << "Seek failure. Shutting down movie.\n";
|
|
|
+ cleanup();
|
|
|
+ _packet_time = t;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ fetch_packet(t);
|
|
|
+ cerr << "Seeking to " << t << " yields " << _packet_time << "\n";
|
|
|
+ if (_packet_time > t) {
|
|
|
+ _packet_time = t;
|
|
|
}
|
|
|
-
|
|
|
- cerr << "Synthesized dummy frame.\n";
|
|
|
- _next_start = _next_start + 1.0;
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
@@ -235,33 +243,49 @@ read_ahead() {
|
|
|
////////////////////////////////////////////////////////////////////
|
|
|
void FfmpegVideo::
|
|
|
fetch_into_buffer(double time, unsigned char *data, bool bgra) {
|
|
|
-
|
|
|
- // If there was an error at any point, fetch black.
|
|
|
+
|
|
|
+ // If there was an error at any point, synthesize black.
|
|
|
if (_format_ctx==0) {
|
|
|
- memset(data,0,size_x()*size_y()*(bgra?4:3));
|
|
|
- _last_start = _next_start;
|
|
|
- _next_start += 1.0;
|
|
|
+ if (data) {
|
|
|
+ memset(data,0,size_x()*size_y()*(bgra?4:3));
|
|
|
+ }
|
|
|
+ _last_start = time;
|
|
|
+ _next_start = time + 1.0;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- AVCodecContext *ctx = _format_ctx->streams[_video_index]->codec;
|
|
|
- nassertv(ctx->width == size_x());
|
|
|
- nassertv(ctx->height == size_y());
|
|
|
-
|
|
|
- if (bgra) {
|
|
|
- _frame_out->data[0] = data + ((ctx->height-1) * ctx->width * 4);
|
|
|
- _frame_out->linesize[0] = ctx->width * -4;
|
|
|
- img_convert((AVPicture *)_frame_out, PIX_FMT_BGRA,
|
|
|
- (AVPicture *)_frame, ctx->pix_fmt, ctx->width, ctx->height);
|
|
|
+ if (time < _last_start) {
|
|
|
+ // Time is in the past.
|
|
|
+ seek(time);
|
|
|
+ while (_packet_time <= time) {
|
|
|
+ fetch_frame();
|
|
|
+ }
|
|
|
+ } else if (time < _next_start) {
|
|
|
+ // Time is in the present: already have the frame.
|
|
|
+ } else if (time < _next_start + _min_fseek) {
|
|
|
+ // Time is in the near future.
|
|
|
+ while (_packet_time <= time) {
|
|
|
+ fetch_frame();
|
|
|
+ }
|
|
|
} else {
|
|
|
- _frame_out->data[0] = data + ((ctx->height-1) * ctx->width * 3);
|
|
|
- _frame_out->linesize[0] = ctx->width * -3;
|
|
|
- img_convert((AVPicture *)_frame_out, PIX_FMT_BGR24,
|
|
|
- (AVPicture *)_frame, ctx->pix_fmt, ctx->width, ctx->height);
|
|
|
+ // Time 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.
|
|
|
+
|
|
|
+ double base = _packet_time;
|
|
|
+ seek(time);
|
|
|
+ if (_packet_time < base) {
|
|
|
+ _min_fseek += (base - _packet_time);
|
|
|
+ }
|
|
|
+ while (_packet_time <= time) {
|
|
|
+ fetch_frame();
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- _last_start = _next_start;
|
|
|
- read_ahead();
|
|
|
+ export_frame(data, bgra);
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////
|