X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=ffmpeg_capture.cpp;h=407f359104cff8444ac56cc9480c77c86366667c;hb=93705b1ae60ecfa71c841467d3476f6d80a70bdc;hp=647b7cbea4b4a96e25b1496b66a0b2b5d476d335;hpb=bcdbb7bdfcfc45b34ccdf36d3950e7e0af6444bd;p=nageru diff --git a/ffmpeg_capture.cpp b/ffmpeg_capture.cpp index 647b7cb..407f359 100644 --- a/ffmpeg_capture.cpp +++ b/ffmpeg_capture.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern "C" { @@ -27,6 +28,7 @@ extern "C" { #include "bmusb/bmusb.h" #include "ffmpeg_raii.h" +#include "ffmpeg_util.h" #include "flags.h" #include "image_input.h" @@ -36,11 +38,36 @@ using namespace std; using namespace std::chrono; using namespace bmusb; +namespace { + +steady_clock::time_point compute_frame_start(int64_t frame_pts, int64_t pts_origin, const AVRational &video_timebase, const steady_clock::time_point &origin, double rate) +{ + const duration pts((frame_pts - pts_origin) * double(video_timebase.num) / double(video_timebase.den)); + return origin + duration_cast(pts / rate); +} + +bool changed_since(const std::string &pathname, const timespec &ts) +{ + if (ts.tv_sec < 0) { + return false; + } + struct stat buf; + if (stat(pathname.c_str(), &buf) != 0) { + fprintf(stderr, "%s: Couldn't check for new version, leaving the old in place.\n", pathname.c_str()); + return false; + } + return (buf.st_mtim.tv_sec != ts.tv_sec || buf.st_mtim.tv_nsec != ts.tv_nsec); +} + +} // namespace + FFmpegCapture::FFmpegCapture(const string &filename, unsigned width, unsigned height) : filename(filename), width(width), height(height) { // Not really used for anything. description = "Video: " + filename; + + avformat_network_init(); // In case someone wants this. } FFmpegCapture::~FFmpegCapture() @@ -68,7 +95,7 @@ void FFmpegCapture::start_bm_capture() return; } running = true; - producer_thread_should_quit = false; + producer_thread_should_quit.unquit(); producer_thread = thread(&FFmpegCapture::producer_thread_func, this); } @@ -78,7 +105,7 @@ void FFmpegCapture::stop_dequeue_thread() return; } running = false; - producer_thread_should_quit = true; + producer_thread_should_quit.quit(); producer_thread.join(); } @@ -107,17 +134,19 @@ void FFmpegCapture::producer_thread_func() snprintf(thread_name, sizeof(thread_name), "FFmpeg_C_%d", card_index); pthread_setname_np(pthread_self(), thread_name); - while (!producer_thread_should_quit) { + while (!producer_thread_should_quit.should_quit()) { string pathname = search_for_file(filename); if (filename.empty()) { fprintf(stderr, "%s not found, sleeping one second and trying again...\n", filename.c_str()); - sleep(1); + send_disconnected_frame(); + producer_thread_should_quit.sleep_for(seconds(1)); continue; } if (!play_video(pathname)) { // Error. fprintf(stderr, "Error when playing %s, sleeping one second and trying again...\n", pathname.c_str()); - sleep(1); + send_disconnected_frame(); + producer_thread_should_quit.sleep_for(seconds(1)); continue; } @@ -130,8 +159,42 @@ void FFmpegCapture::producer_thread_func() } } +void FFmpegCapture::send_disconnected_frame() +{ + // Send an empty frame to signal that we have no signal anymore. + FrameAllocator::Frame video_frame = video_frame_allocator->alloc_frame(); + if (video_frame.data) { + VideoFormat video_format; + video_format.width = width; + video_format.height = height; + video_format.stride = width * 4; + video_format.frame_rate_nom = 60; + video_format.frame_rate_den = 1; + video_format.is_connected = false; + + video_frame.len = width * height * 4; + memset(video_frame.data, 0, video_frame.len); + + frame_callback(timecode++, + video_frame, /*video_offset=*/0, video_format, + FrameAllocator::Frame(), /*audio_offset=*/0, AudioFormat()); + } +} + bool FFmpegCapture::play_video(const string &pathname) { + // Note: Call before open, not after; otherwise, there's a race. + // (There is now, too, but it tips the correct way. We could use fstat() + // if we had the file descriptor.) + timespec last_modified; + struct stat buf; + if (stat(pathname.c_str(), &buf) != 0) { + // Probably some sort of protocol, so can't stat. + last_modified.tv_sec = -1; + } else { + last_modified = buf.st_mtim; + } + auto format_ctx = avformat_open_input_unique(pathname.c_str(), nullptr, nullptr); if (format_ctx == nullptr) { fprintf(stderr, "%s: Error opening file\n", pathname.c_str()); @@ -143,25 +206,14 @@ bool FFmpegCapture::play_video(const string &pathname) return false; } - int video_stream_index = -1, audio_stream_index = -1; - AVRational video_timebase{ 1, 1 }; - for (unsigned i = 0; i < format_ctx->nb_streams; ++i) { - if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && - video_stream_index == -1) { - video_stream_index = i; - video_timebase = format_ctx->streams[i]->time_base; - } - if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && - audio_stream_index == -1) { - audio_stream_index = i; - } - } + int video_stream_index = find_stream_index(format_ctx.get(), AVMEDIA_TYPE_VIDEO); if (video_stream_index == -1) { fprintf(stderr, "%s: No video stream found\n", pathname.c_str()); return false; } const AVCodecParameters *codecpar = format_ctx->streams[video_stream_index]->codecpar; + AVRational video_timebase = format_ctx->streams[video_stream_index]->time_base; AVCodecContextWithDeleter codec_ctx = avcodec_alloc_context3_unique(nullptr); if (avcodec_parameters_to_context(codec_ctx.get(), codecpar) < 0) { fprintf(stderr, "%s: Cannot fill codec parameters\n", pathname.c_str()); @@ -179,13 +231,44 @@ bool FFmpegCapture::play_video(const string &pathname) unique_ptr codec_ctx_cleanup( codec_ctx.get(), avcodec_close); - steady_clock::time_point start = steady_clock::now(); + internal_rewind(); + double rate = 1.0; unique_ptr sws_ctx(nullptr, sws_freeContext); int sws_last_width = -1, sws_last_height = -1; // Main loop. - while (!producer_thread_should_quit) { + while (!producer_thread_should_quit.should_quit()) { + // Process any queued commands from other threads. + vector commands; + { + lock_guard lock(queue_mu); + swap(commands, command_queue); + } + for (const QueuedCommand &cmd : commands) { + switch (cmd.command) { + case QueuedCommand::REWIND: + if (av_seek_frame(format_ctx.get(), /*stream_index=*/-1, /*timestamp=*/0, /*flags=*/0) < 0) { + fprintf(stderr, "%s: Rewind failed, stopping play.\n", pathname.c_str()); + } + // If the file has changed since last time, return to get it reloaded. + // Note that depending on how you move the file into place, you might + // end up corrupting the one you're already playing, so this path + // might not trigger. + if (changed_since(pathname, last_modified)) { + return true; + } + internal_rewind(); + break; + + case QueuedCommand::CHANGE_RATE: + start = next_frame_start; + pts_origin = last_pts; + rate = cmd.new_rate; + break; + } + } + // Read packets until we have a frame or there are none left. int frame_finished = 0; AVFrameWithDeleter frame = av_frame_alloc_unique(); @@ -226,14 +309,21 @@ bool FFmpegCapture::play_video(const string &pathname) fprintf(stderr, "%s: Rewind failed, not looping.\n", pathname.c_str()); return true; } - start = steady_clock::now(); + // If the file has changed since last time, return to get it reloaded. + // Note that depending on how you move the file into place, you might + // end up corrupting the one you're already playing, so this path + // might not trigger. + if (changed_since(pathname, last_modified)) { + return true; + } + internal_rewind(); continue; } if (sws_ctx == nullptr || sws_last_width != frame->width || sws_last_height != frame->height) { sws_ctx.reset( sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format, - width, height, AV_PIX_FMT_RGBA, + width, height, AV_PIX_FMT_BGRA, SWS_BICUBIC, nullptr, nullptr, nullptr)); sws_last_width = frame->width; sws_last_height = frame->height; @@ -249,11 +339,16 @@ bool FFmpegCapture::play_video(const string &pathname) video_format.stride = width * 4; video_format.frame_rate_nom = video_timebase.den; video_format.frame_rate_den = av_frame_get_pkt_duration(frame.get()) * video_timebase.num; + if (video_format.frame_rate_nom == 0 || video_format.frame_rate_den == 0) { + // Invalid frame rate. + video_format.frame_rate_nom = 60; + video_format.frame_rate_den = 1; + } video_format.has_signal = true; video_format.is_connected = true; - const duration pts(frame->pts * double(video_timebase.num) / double(video_timebase.den)); - const steady_clock::time_point frame_start = start + duration_cast(pts); + next_frame_start = compute_frame_start(frame->pts, pts_origin, video_timebase, start, rate); + last_pts = frame->pts; FrameAllocator::Frame video_frame = video_frame_allocator->alloc_frame(); if (video_frame.data != nullptr) { @@ -261,7 +356,7 @@ bool FFmpegCapture::play_video(const string &pathname) int linesizes[4] = { int(video_format.stride), 0, 0, 0 }; sws_scale(sws_ctx.get(), frame->data, frame->linesize, 0, frame->height, pic_data, linesizes); video_frame.len = video_format.stride * height; - video_frame.received_timestamp = frame_start; + video_frame.received_timestamp = next_frame_start; } FrameAllocator::Frame audio_frame; @@ -269,10 +364,16 @@ bool FFmpegCapture::play_video(const string &pathname) audio_format.bits_per_sample = 32; audio_format.num_channels = 8; - this_thread::sleep_until(frame_start); + producer_thread_should_quit.sleep_until(next_frame_start); frame_callback(timecode++, video_frame, 0, video_format, audio_frame, 0, audio_format); } return true; } + +void FFmpegCapture::internal_rewind() +{ + pts_origin = last_pts = 0; + start = next_frame_start = steady_clock::now(); +}