X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=nageru%2Fmixer.cpp;h=c9f7ed78861b7bf3136c913038eff9c13a0254c5;hb=1924bea4fde26067d8ee81cfd12f2dd66c5b2cad;hp=c91857e62fbab09b3677fcebba41402235feabc6;hpb=9ffd4f03f314cc6e0254449593def95c9bc203d6;p=nageru diff --git a/nageru/mixer.cpp b/nageru/mixer.cpp index c91857e..c9f7ed7 100644 --- a/nageru/mixer.cpp +++ b/nageru/mixer.cpp @@ -44,6 +44,7 @@ #include "shared/disk_space_estimator.h" #include "ffmpeg_capture.h" #include "flags.h" +#include "image_input.h" #include "input_mapping.h" #include "shared/metrics.h" #include "mjpeg_encoder.h" @@ -152,11 +153,8 @@ void ensure_texture_resolution(PBOFrameAllocator::Userdata *userdata, unsigned f case PixelFormat_8BitBGRA: glBindTexture(GL_TEXTURE_2D, userdata->tex_rgba[field]); check_error(); - if (global_flags.can_disable_srgb_decoder) { // See the comments in tweaked_inputs.h. - glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, width, height, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); - } else { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); - } + // NOTE: sRGB may be disabled by sRGBSwitchingFlatInput. + glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, width, height, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); check_error(); break; default: @@ -306,16 +304,18 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) num_cards(num_cards), mixer_surface(create_surface(format)), h264_encoder_surface(create_surface(format)), - decklink_output_surface(create_surface(format)) + decklink_output_surface(create_surface(format)), + image_update_surface(create_surface(format)) { memcpy(ycbcr_interpretation, global_flags.ycbcr_interpretation, sizeof(ycbcr_interpretation)); CHECK(init_movit(MOVIT_SHADER_DIR, MOVIT_DEBUG_OFF)); check_error(); - // This nearly always should be true. - global_flags.can_disable_srgb_decoder = - epoxy_has_gl_extension("GL_EXT_texture_sRGB_decode") && - epoxy_has_gl_extension("GL_ARB_sampler_objects"); + if (!epoxy_has_gl_extension("GL_EXT_texture_sRGB_decode") || + !epoxy_has_gl_extension("GL_ARB_sampler_objects")) { + fprintf(stderr, "Nageru requires GL_EXT_texture_sRGB_decode and GL_ARB_sampler_objects to run.\n"); + exit(1); + } // Since we allow non-bouncing 4:2:2 YCbCrInputs, effective subpixel precision // will be halved when sampling them, and we need to compensate here. @@ -370,10 +370,10 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) audio_mixer.reset(new AudioMixer(num_cards, video_inputs.size())); httpd.add_endpoint("/channels", bind(&Mixer::get_channels_json, this), HTTPD::ALLOW_ALL_ORIGINS); - for (int channel_idx = 2; channel_idx < theme->get_num_channels(); ++channel_idx) { + for (int channel_idx = 0; channel_idx < theme->get_num_channels(); ++channel_idx) { char url[256]; - snprintf(url, sizeof(url), "/channels/%d/color", channel_idx); - httpd.add_endpoint(url, bind(&Mixer::get_channel_color_http, this, unsigned(channel_idx)), HTTPD::ALLOW_ALL_ORIGINS); + snprintf(url, sizeof(url), "/channels/%d/color", channel_idx + 2); + httpd.add_endpoint(url, bind(&Mixer::get_channel_color_http, this, unsigned(channel_idx + 2)), HTTPD::ALLOW_ALL_ORIGINS); } // Start listening for clients only once VideoEncoder has written its header, if any. @@ -500,10 +500,14 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) } output_jitter_history.register_metrics({{ "card", "output" }}); + + ImageInput::start_update_thread(image_update_surface); } Mixer::~Mixer() { + ImageInput::end_update_thread(); + if (mjpeg_encoder != nullptr) { mjpeg_encoder->stop(); } @@ -785,6 +789,34 @@ void Mixer::bm_frame(unsigned card_index, uint16_t timecode, if (num_samples > 0) { audio_mixer->add_audio(device, audio_frame.data + audio_offset, num_samples, audio_format, audio_frame.received_timestamp); + + // Audio for the MJPEG stream. We don't resample; audio that's not in 48 kHz + // just gets dropped for now. + // + // Only bother doing MJPEG encoding if there are any connected clients + // that want the stream. + if (httpd.get_num_connected_multicam_clients() > 0) { + vector converted_samples = convert_audio_to_fixed32(audio_frame.data + audio_offset, num_samples, audio_format, 2); + lock_guard lock(card_mutex); + if (card->new_raw_audio.empty()) { + card->new_raw_audio = move(converted_samples); + } else { + // For raw audio, we don't really synchronize audio and video; + // we just put the audio in frame by frame, and if a video frame is + // dropped, we still keep the audio, which means it will be added + // to the beginning of the next frame. It would probably be better + // to move the audio pts earlier to show this, but most players can + // live with some jitter, and in a lot of ways, it's much nicer for + // Futatabi to have all audio locked to a video frame. + card->new_raw_audio.insert(card->new_raw_audio.end(), converted_samples.begin(), converted_samples.end()); + + // Truncate to one second, just to be sure we don't have infinite buildup in case of weirdness. + if (card->new_raw_audio.size() > OUTPUT_FREQUENCY * 2) { + size_t excess_samples = card->new_raw_audio.size() - OUTPUT_FREQUENCY * 2; + card->new_raw_audio.erase(card->new_raw_audio.begin(), card->new_raw_audio.begin() + excess_samples); + } + } + } } // Done with the audio, so release it. @@ -1038,7 +1070,8 @@ void Mixer::thread_func() assert(master_card_index < num_cards + num_video_inputs); } - OutputFrameInfo output_frame_info = get_one_frame_from_each_card(master_card_index, master_card_is_output, new_frames, has_new_frame); + vector raw_audio[MAX_VIDEO_CARDS]; // For MJPEG encoding. + OutputFrameInfo output_frame_info = get_one_frame_from_each_card(master_card_index, master_card_is_output, new_frames, has_new_frame, raw_audio); schedule_audio_resampling_tasks(output_frame_info.dropped_frames, output_frame_info.num_samples, output_frame_info.frame_duration, output_frame_info.is_preroll, output_frame_info.frame_timestamp); stats_dropped_frames += output_frame_info.dropped_frames; @@ -1084,7 +1117,7 @@ void Mixer::thread_func() if (new_frame->frame->data_copy != nullptr) { int mjpeg_card_index = mjpeg_encoder->get_mjpeg_stream_for_card(card_index); if (mjpeg_card_index != -1) { - mjpeg_encoder->upload_frame(pts_int, mjpeg_card_index, new_frame->frame, new_frame->video_format, new_frame->y_offset, new_frame->cbcr_offset); + mjpeg_encoder->upload_frame(pts_int, mjpeg_card_index, new_frame->frame, new_frame->video_format, new_frame->y_offset, new_frame->cbcr_offset, move(raw_audio[card_index])); } } } @@ -1198,7 +1231,7 @@ pair Mixer::get_channel_color_http(unsigned channel_idx) return make_pair(theme->get_channel_color(channel_idx), "text/plain"); } -Mixer::OutputFrameInfo Mixer::get_one_frame_from_each_card(unsigned master_card_index, bool master_card_is_output, CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS], bool has_new_frame[MAX_VIDEO_CARDS]) +Mixer::OutputFrameInfo Mixer::get_one_frame_from_each_card(unsigned master_card_index, bool master_card_is_output, CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS], bool has_new_frame[MAX_VIDEO_CARDS], vector raw_audio[MAX_VIDEO_CARDS]) { OutputFrameInfo output_frame_info; start: @@ -1237,7 +1270,7 @@ start: // we dropped. (may_have_dropped_last_frame is set whenever we // trim the queue completely away, and cleared when we actually // get a new frame.) - ((CEFCapture *)card->capture.get())->request_new_frame(); + ((CEFCapture *)card->capture.get())->request_new_frame(/*ignore_if_locked=*/true); } #endif } else { @@ -1246,6 +1279,8 @@ start: card->new_frames.pop_front(); card->new_frames_changed.notify_all(); } + + raw_audio[card_index] = move(card->new_raw_audio); } if (!master_card_is_output) {