X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=ffmpeg_capture.cpp;h=5f95da240448a5ec90efd45cd9c80b9b1a4dd2e3;hb=refs%2Fheads%2Fffmpeg-audio-only;hp=6a235f5e4305a134a43b19918782f8befc7e4198;hpb=66e61a4fe04650471d4aa95eaa596557a3cb74ca;p=nageru diff --git a/ffmpeg_capture.cpp b/ffmpeg_capture.cpp index 6a235f5..5f95da2 100644 --- a/ffmpeg_capture.cpp +++ b/ffmpeg_capture.cpp @@ -208,7 +208,6 @@ YCbCrFormat decode_ycbcr_format(const AVPixFmtDescriptor *desc, const AVFrame *f FFmpegCapture::FFmpegCapture(const string &filename, unsigned width, unsigned height) : filename(filename), width(width), height(height), video_timebase{1, 1} { - // Not really used for anything. description = "Video: " + filename; last_frame = steady_clock::now(); @@ -283,9 +282,15 @@ void FFmpegCapture::producer_thread_func() pthread_setname_np(pthread_self(), thread_name); 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()); + string filename_copy; + { + lock_guard lock(filename_mu); + filename_copy = filename; + } + + string pathname = search_for_file(filename_copy); + if (pathname.empty()) { + fprintf(stderr, "%s not found, sleeping one second and trying again...\n", filename_copy.c_str()); send_disconnected_frame(); producer_thread_should_quit.sleep_for(seconds(1)); continue; @@ -322,6 +327,7 @@ void FFmpegCapture::send_disconnected_frame() if (pixel_format == bmusb::PixelFormat_8BitBGRA) { video_format.stride = width * 4; video_frame.len = width * height * 4; + memset(video_frame.data, 0, video_frame.len); } else { video_format.stride = width; current_frame_ycbcr_format.luma_coefficients = YCBCR_REC_709; @@ -334,8 +340,9 @@ void FFmpegCapture::send_disconnected_frame() current_frame_ycbcr_format.cr_x_position = 0.0f; current_frame_ycbcr_format.cr_y_position = 0.0f; video_frame.len = width * height * 2; + memset(video_frame.data, 0, width * height); + memset(video_frame.data + width * height, 128, width * height); // Valid for both NV12 and planar. } - memset(video_frame.data, 0, video_frame.len); frame_callback(-1, AVRational{1, TIMEBASE}, -1, AVRational{1, TIMEBASE}, timecode++, video_frame, /*video_offset=*/0, video_format, @@ -373,29 +380,36 @@ bool FFmpegCapture::play_video(const string &pathname) } 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; - } - int audio_stream_index = find_stream_index(format_ctx.get(), AVMEDIA_TYPE_AUDIO); - // Open video decoder. - const AVCodecParameters *video_codecpar = format_ctx->streams[video_stream_index]->codecpar; - AVCodec *video_codec = avcodec_find_decoder(video_codecpar->codec_id); - video_timebase = format_ctx->streams[video_stream_index]->time_base; - AVCodecContextWithDeleter video_codec_ctx = avcodec_alloc_context3_unique(nullptr); - if (avcodec_parameters_to_context(video_codec_ctx.get(), video_codecpar) < 0) { - fprintf(stderr, "%s: Cannot fill video codec parameters\n", pathname.c_str()); - return false; - } - if (video_codec == nullptr) { - fprintf(stderr, "%s: Cannot find video decoder\n", pathname.c_str()); + if (video_stream_index == -1 && audio_stream_index == -1) { + fprintf(stderr, "%s: No audio nor video stream found\n", pathname.c_str()); return false; } - if (avcodec_open2(video_codec_ctx.get(), video_codec, nullptr) < 0) { - fprintf(stderr, "%s: Cannot open video decoder\n", pathname.c_str()); - return false; + if (video_stream_index == -1) { + fprintf(stderr, "%s: No video stream found, assuming audio-only.\n", pathname.c_str()); + } + const bool audio_only_stream = (video_stream_index == -1); + + // Open video decoder, if we have video. + AVCodecContextWithDeleter video_codec_ctx; + if (video_stream_index != -1) { + const AVCodecParameters *video_codecpar = format_ctx->streams[video_stream_index]->codecpar; + AVCodec *video_codec = avcodec_find_decoder(video_codecpar->codec_id); + video_timebase = format_ctx->streams[video_stream_index]->time_base; + video_codec_ctx = avcodec_alloc_context3_unique(nullptr); + if (avcodec_parameters_to_context(video_codec_ctx.get(), video_codecpar) < 0) { + fprintf(stderr, "%s: Cannot fill video codec parameters\n", pathname.c_str()); + return false; + } + if (video_codec == nullptr) { + fprintf(stderr, "%s: Cannot find video decoder\n", pathname.c_str()); + return false; + } + if (avcodec_open2(video_codec_ctx.get(), video_codec, nullptr) < 0) { + fprintf(stderr, "%s: Cannot open video decoder\n", pathname.c_str()); + return false; + } } unique_ptr video_codec_ctx_cleanup( video_codec_ctx.get(), avcodec_close); @@ -441,7 +455,7 @@ bool FFmpegCapture::play_video(const string &pathname) if (error) { return false; } - if (frame == nullptr) { + if (frame == nullptr && !(audio_only_stream && audio_frame->len > 0)) { // EOF. Loop back to the start if we can. if (av_seek_frame(format_ctx.get(), /*stream_index=*/-1, /*timestamp=*/0, /*flags=*/0) < 0) { fprintf(stderr, "%s: Rewind failed, not looping.\n", pathname.c_str()); @@ -464,52 +478,75 @@ bool FFmpegCapture::play_video(const string &pathname) continue; } - VideoFormat video_format = construct_video_format(frame.get(), video_timebase); - UniqueFrame video_frame = make_video_frame(frame.get(), pathname, &error); - if (error) { - return false; + VideoFormat video_format; + UniqueFrame video_frame; + if (!audio_only_stream) { + video_format = construct_video_format(frame.get(), video_timebase); + video_frame = make_video_frame(frame.get(), pathname, &error); + if (error) { + return false; + } } - for ( ;; ) { + int64_t frame_pts = audio_only_stream ? audio_pts : frame->pts; + AVRational timebase = audio_only_stream ? audio_timebase : video_timebase; + for ( ;; ) { // Try sending the frame in a loop as long as we get interrupted (then break). if (last_pts == 0 && pts_origin == 0) { - pts_origin = frame->pts; + pts_origin = frame_pts; } - next_frame_start = compute_frame_start(frame->pts, pts_origin, video_timebase, start, rate); - if (first_frame && last_frame_was_connected) { - // If reconnect took more than one second, this is probably a live feed, - // and we should reset the resampler. (Or the rate is really, really low, - // in which case a reset on the first frame is fine anyway.) - if (duration(next_frame_start - last_frame).count() >= 1.0) { - last_frame_was_connected = false; + next_frame_start = compute_frame_start(frame_pts, pts_origin, timebase, start, rate); + if (audio_only_stream) { + audio_frame->received_timestamp = next_frame_start; + } else { + if (first_frame && last_frame_was_connected) { + // If reconnect took more than one second, this is probably a live feed, + // and we should reset the resampler. (Or the rate is really, really low, + // in which case a reset on the first frame is fine anyway.) + if (duration(next_frame_start - last_frame).count() >= 1.0) { + last_frame_was_connected = false; + } + } + video_frame->received_timestamp = next_frame_start; + + // The easiest way to get all the rate conversions etc. right is to move the + // audio PTS into the video PTS timebase and go from there. (We'll get some + // rounding issues, but they should not be a big problem.) + int64_t audio_pts_as_video_pts = av_rescale_q(audio_pts, audio_timebase, video_timebase); + audio_frame->received_timestamp = compute_frame_start(audio_pts_as_video_pts, pts_origin, video_timebase, start, rate); + + if (audio_frame->len != 0) { + // The received timestamps in Nageru are measured after we've just received the frame. + // However, pts (especially audio pts) is at the _beginning_ of the frame. + // If we have locked audio, the distinction doesn't really matter, as pts is + // on a relative scale and a fixed offset is fine. But if we don't, we will have + // a different number of samples each time, which will cause huge audio jitter + // and throw off the resampler. + // + // In a sense, we should have compensated by adding the frame and audio lengths + // to video_frame->received_timestamp and audio_frame->received_timestamp respectively, + // but that would mean extra waiting in sleep_until(). All we need is that they + // are correct relative to each other, though (and to the other frames we send), + // so just align the end of the audio frame, and we're fine. + size_t num_samples = (audio_frame->len * 8) / audio_format.bits_per_sample / audio_format.num_channels; + double offset = double(num_samples) / OUTPUT_FREQUENCY - + double(video_format.frame_rate_den) / video_format.frame_rate_nom; + audio_frame->received_timestamp += duration_cast(duration(offset)); } - } - video_frame->received_timestamp = next_frame_start; - - // The easiest way to get all the rate conversions etc. right is to move the - // audio PTS into the video PTS timebase and go from there. (We'll get some - // rounding issues, but they should not be a big problem.) - int64_t audio_pts_as_video_pts = av_rescale_q(audio_pts, audio_timebase, video_timebase); - audio_frame->received_timestamp = compute_frame_start(audio_pts_as_video_pts, pts_origin, video_timebase, start, rate); - - if (audio_frame->len != 0) { - // The received timestamps in Nageru are measured after we've just received the frame. - // However, pts (especially audio pts) is at the _beginning_ of the frame. - // If we have locked audio, the distinction doesn't really matter, as pts is - // on a relative scale and a fixed offset is fine. But if we don't, we will have - // a different number of samples each time, which will cause huge audio jitter - // and throw off the resampler. - // - // In a sense, we should have compensated by adding the frame and audio lengths - // to video_frame->received_timestamp and audio_frame->received_timestamp respectively, - // but that would mean extra waiting in sleep_until(). All we need is that they - // are correct relative to each other, though (and to the other frames we send), - // so just align the end of the audio frame, and we're fine. - size_t num_samples = (audio_frame->len * 8) / audio_format.bits_per_sample / audio_format.num_channels; - double offset = double(num_samples) / OUTPUT_FREQUENCY - - double(video_format.frame_rate_den) / video_format.frame_rate_nom; - audio_frame->received_timestamp += duration_cast(duration(offset)); } + steady_clock::time_point now = steady_clock::now(); + if (duration(now - next_frame_start).count() >= 0.1) { + // If we don't have enough CPU to keep up, or if we have a live stream + // where the initial origin was somehow wrong, we could be behind indefinitely. + // In particular, this will give the audio resampler problems as it tries + // to speed up to reduce the delay, hitting the low end of the buffer every time. + fprintf(stderr, "%s: Playback %.0f ms behind, resetting time scale\n", + pathname.c_str(), + 1e3 * duration(now - next_frame_start).count()); + pts_origin = frame_pts; + start = next_frame_start = now; + timecode += MAX_FPS * 2 + 1; + } bool finished_wakeup = producer_thread_should_quit.sleep_until(next_frame_start); if (finished_wakeup) { if (audio_frame->len > 0) { @@ -522,7 +559,7 @@ bool FFmpegCapture::play_video(const string &pathname) // audio discontinuity.) timecode += MAX_FPS * 2 + 1; } - frame_callback(frame->pts, video_timebase, audio_pts, audio_timebase, timecode++, + frame_callback(frame_pts, video_timebase, audio_pts, audio_timebase, timecode++, video_frame.get_and_release(), 0, video_format, audio_frame.get_and_release(), 0, audio_format); first_frame = false; @@ -551,7 +588,7 @@ bool FFmpegCapture::play_video(const string &pathname) } } } - last_pts = frame->pts; + last_pts = frame_pts; } return true; } @@ -665,14 +702,18 @@ AVFrameWithDeleter FFmpegCapture::decode_frame(AVFormatContext *format_ctx, AVCo } } - // Decode video, if we have a frame. - int err = avcodec_receive_frame(video_codec_ctx, video_avframe.get()); - if (err == 0) { - frame_finished = true; - break; - } else if (err != AVERROR(EAGAIN)) { - fprintf(stderr, "%s: Cannot receive frame from video codec.\n", pathname.c_str()); - *error = true; + if (video_codec_ctx != nullptr) { + // Decode video, if we have a frame. + int err = avcodec_receive_frame(video_codec_ctx, video_avframe.get()); + if (err == 0) { + frame_finished = true; + break; + } else if (err != AVERROR(EAGAIN)) { + fprintf(stderr, "%s: Cannot receive frame from video codec.\n", pathname.c_str()); + *error = true; + return AVFrameWithDeleter(nullptr); + } + } else { return AVFrameWithDeleter(nullptr); } } while (!eof); @@ -731,7 +772,7 @@ void FFmpegCapture::convert_audio(const AVFrame *audio_avframe, FrameAllocator:: } av_opt_set_int(resampler, "in_channel_layout", channel_layout, 0); - av_opt_set_int(resampler, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_int(resampler, "out_channel_layout", AV_CH_LAYOUT_STEREO_DOWNMIX, 0); av_opt_set_int(resampler, "in_sample_rate", av_frame_get_sample_rate(audio_avframe), 0); av_opt_set_int(resampler, "out_sample_rate", OUTPUT_FREQUENCY, 0); av_opt_set_int(resampler, "in_sample_fmt", audio_avframe->format, 0);