# Mixer objects
AUDIO_MIXER_OBJS = audio_mixer.o alsa_input.o alsa_pool.o ebu_r128_proc.o stereocompressor.o resampling_queue.o flags.o correlation_measurer.o filter.o input_mapping.o state.pb.o
-OBJS += chroma_subsampler.o mixer.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o httpd.o flags.o image_input.o alsa_output.o disk_space_estimator.o print_latency.o $(AUDIO_MIXER_OBJS)
+OBJS += chroma_subsampler.o mixer.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o httpd.o flags.o image_input.o alsa_output.o disk_space_estimator.o print_latency.o timecode_renderer.o $(AUDIO_MIXER_OBJS)
# Streaming and encoding objects
OBJS += quicksync_encoder.o x264_encoder.o x264_speed_control.o video_encoder.o metacube2.o mux.o audio_encoder.o ffmpeg_raii.o
OPTION_OUTPUT_YCBCR_COEFFICIENTS,
OPTION_OUTPUT_BUFFER_FRAMES,
OPTION_OUTPUT_SLOP_FRAMES,
+ OPTION_TIMECODE_STREAM,
+ OPTION_TIMECODE_STDOUT,
};
void usage()
fprintf(stderr, " --output-slop-frames=NUM if more less than this number of frames behind for\n");
fprintf(stderr, " --output-card, try to submit anyway instead of\n");
fprintf(stderr, " dropping the frame (default 0.5)\n");
+ fprintf(stderr, " --timecode-stream show timestamp and timecode in stream\n");
+ fprintf(stderr, " --timecode-stdout show timestamp and timecode on standard output\n");
}
void parse_flags(int argc, char * const argv[])
{ "output-ycbcr-coefficients", required_argument, 0, OPTION_OUTPUT_YCBCR_COEFFICIENTS },
{ "output-buffer-frames", required_argument, 0, OPTION_OUTPUT_BUFFER_FRAMES },
{ "output-slop-frames", required_argument, 0, OPTION_OUTPUT_SLOP_FRAMES },
+ { "timecode-stream", no_argument, 0, OPTION_TIMECODE_STREAM },
+ { "timecode-stdout", no_argument, 0, OPTION_TIMECODE_STDOUT },
{ 0, 0, 0, 0 }
};
vector<string> theme_dirs;
case OPTION_OUTPUT_SLOP_FRAMES:
global_flags.output_slop_frames = atof(optarg);
break;
+ case OPTION_TIMECODE_STREAM:
+ global_flags.display_timecode_in_stream = true;
+ break;
+ case OPTION_TIMECODE_STDOUT:
+ global_flags.display_timecode_on_stdout = true;
+ break;
case OPTION_HELP:
usage();
exit(0);
double output_buffer_frames = 6.0;
double output_slop_frames = 0.5;
int max_input_queue_frames = 6;
+ bool display_timecode_in_stream = false;
+ bool display_timecode_on_stdout = false;
};
extern Flags global_flags;
connect(ui->multichannel_audio_mode, &QAction::triggered, this, &MainWindow::multichannel_audio_mode_triggered);
connect(ui->input_mapping_action, &QAction::triggered, this, &MainWindow::input_mapping_triggered);
connect(ui->midi_mapping_action, &QAction::triggered, this, &MainWindow::midi_mapping_triggered);
+ connect(ui->timecode_stream_action, &QAction::triggered, this, &MainWindow::timecode_stream_triggered);
+ connect(ui->timecode_stdout_action, &QAction::triggered, this, &MainWindow::timecode_stdout_triggered);
+
+ ui->timecode_stream_action->setChecked(global_flags.display_timecode_in_stream);
+ ui->timecode_stdout_action->setChecked(global_flags.display_timecode_on_stdout);
if (global_flags.x264_video_to_http) {
connect(ui->x264_bitrate_action, &QAction::triggered, this, &MainWindow::x264_bitrate_triggered);
MIDIMappingDialog(&midi_mapper).exec();
}
+void MainWindow::timecode_stream_triggered()
+{
+ global_mixer->set_display_timecode_in_stream(ui->timecode_stream_action->isChecked());
+}
+
+void MainWindow::timecode_stdout_triggered()
+{
+ global_mixer->set_display_timecode_on_stdout(ui->timecode_stdout_action->isChecked());
+}
+
void MainWindow::gain_staging_knob_changed(unsigned bus_index, int value)
{
if (bus_index == 0) {
void multichannel_audio_mode_triggered();
void input_mapping_triggered();
void midi_mapping_triggered();
+ void timecode_stream_triggered();
+ void timecode_stdout_triggered();
void transition_clicked(int transition_number);
void channel_clicked(int channel_number);
void wb_button_clicked(int channel_number);
#include "ref_counted_gl_sync.h"
#include "resampling_queue.h"
#include "timebase.h"
+#include "timecode_renderer.h"
#include "video_encoder.h"
class IDeckLink;
chroma_subsampler.reset(new ChromaSubsampler(resource_pool.get()));
+ timecode_renderer.reset(new TimecodeRenderer(resource_pool.get(), global_flags.width, global_flags.height));
+ display_timecode_in_stream = global_flags.display_timecode_in_stream;
+ display_timecode_on_stdout = global_flags.display_timecode_on_stdout;
+
if (global_flags.enable_alsa_output) {
alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
}
steady_clock::time_point start, now;
start = steady_clock::now();
- int frame = 0;
int stats_dropped_frames = 0;
while (!should_quit) {
int64_t frame_duration = output_frame_info.frame_duration;
render_one_frame(frame_duration);
- ++frame;
+ ++frame_num;
pts_int += frame_duration;
now = steady_clock::now();
double elapsed = duration<double>(now - start).count();
- if (frame % 100 == 0) {
+ if (frame_num % 100 == 0) {
printf("%d frames (%d dropped) in %.3f seconds = %.1f fps (%.1f ms/frame)",
- frame, stats_dropped_frames, elapsed, frame / elapsed,
- 1e3 * elapsed / frame);
+ frame_num, stats_dropped_frames, elapsed, frame_num / elapsed,
+ 1e3 * elapsed / frame_num);
// chain->print_phase_timing();
// Check our memory usage, to see if we are close to our mlockall()
if (should_cut.exchange(false)) { // Test and clear.
- video_encoder->do_cut(frame);
+ video_encoder->do_cut(frame_num);
}
#if 0
void Mixer::render_one_frame(int64_t duration)
{
+ // Determine the time code for this frame before we start rendering.
+ string timecode_text = timecode_renderer->get_timecode_text(double(pts_int) / TIMEBASE, frame_num);
+ if (display_timecode_on_stdout) {
+ printf("Timecode: '%s'\n", timecode_text.c_str());
+ }
+
// Get the main chain from the theme, and set its state immediately.
Theme::Chain theme_main_chain = theme->get_chain(0, pts(), global_flags.width, global_flags.height, input_state);
EffectChain *chain = theme_main_chain.chain;
GLuint fbo = resource_pool->create_fbo(y_tex, cbcr_full_tex, rgba_tex);
check_error();
chain->render_to_fbo(fbo, global_flags.width, global_flags.height);
+
+ if (display_timecode_in_stream) {
+ // Render the timecode on top.
+ timecode_renderer->render_timecode(fbo, timecode_text);
+ }
+
resource_pool->release_fbo(fbo);
chroma_subsampler->subsample_chroma(cbcr_full_tex, global_flags.width, global_flags.height, cbcr_tex);
class ALSAOutput;
class ChromaSubsampler;
class DeckLinkOutput;
+class TimecodeRenderer;
class QSurface;
class QSurfaceFormat;
desired_output_video_mode = mode;
}
+ void set_display_timecode_in_stream(bool enable) {
+ display_timecode_in_stream = enable;
+ }
+
+ void set_display_timecode_on_stdout(bool enable) {
+ display_timecode_on_stdout = enable;
+ }
+
private:
struct CaptureCard;
void thread_func();
void handle_hotplugged_cards();
void schedule_audio_resampling_tasks(unsigned dropped_frames, int num_samples_per_frame, int length_per_frame, bool is_preroll, std::chrono::steady_clock::time_point frame_timestamp);
+ std::string get_timecode_text() const;
void render_one_frame(int64_t duration);
void audio_thread_func();
void release_display_frame(DisplayFrame *frame);
std::unique_ptr<ChromaSubsampler> chroma_subsampler;
std::unique_ptr<VideoEncoder> video_encoder;
+ std::unique_ptr<TimecodeRenderer> timecode_renderer;
+ std::atomic<bool> display_timecode_in_stream{false};
+ std::atomic<bool> display_timecode_on_stdout{false};
+
// Effects part of <display_chain>. Owned by <display_chain>.
movit::FlatInput *display_input;
int64_t pts_int = 0; // In TIMEBASE units.
+ unsigned frame_num = 0;
// Accumulated errors in number of 1/TIMEBASE audio samples. If OUTPUT_FREQUENCY divided by
// frame rate is integer, will always stay zero.
--- /dev/null
+#include "timecode_renderer.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <QImage>
+#include <QPainter>
+
+#include <epoxy/gl.h>
+#include <movit/effect_util.h>
+#include <movit/resource_pool.h>
+#include <movit/util.h>
+#include <sys/time.h>
+
+using namespace std;
+using namespace movit;
+
+TimecodeRenderer::TimecodeRenderer(movit::ResourcePool *resource_pool, unsigned display_width, unsigned display_height)
+ : resource_pool(resource_pool), display_width(display_width), display_height(display_height), height(28)
+{
+ string vert_shader =
+ "#version 130 \n"
+ " \n"
+ "in vec2 position; \n"
+ "in vec2 texcoord; \n"
+ "out vec2 tc0; \n"
+ " \n"
+ "void main() \n"
+ "{ \n"
+ " // The result of glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0) is: \n"
+ " // \n"
+ " // 2.000 0.000 0.000 -1.000 \n"
+ " // 0.000 2.000 0.000 -1.000 \n"
+ " // 0.000 0.000 -2.000 -1.000 \n"
+ " // 0.000 0.000 0.000 1.000 \n"
+ " gl_Position = vec4(2.0 * position.x - 1.0, 2.0 * position.y - 1.0, -1.0, 1.0); \n"
+ " tc0 = texcoord; \n"
+ "} \n";
+ string frag_shader =
+ "#version 130 \n"
+ "in vec2 tc0; \n"
+ "uniform sampler2D tex; \n"
+ "out vec4 Y, CbCr, RGBA; \n"
+ "void main() { \n"
+ " vec4 gray = texture(tex, tc0); \n"
+ " RGBA = gray.rrra; \n"
+ " gray.r = gray.r * ((235.0-16.0)/255.0) + 16.0/255.0; \n" // Limited-range Y'CbCr.
+ " Y = gray.rrra; \n"
+ " CbCr = vec4(128.0/255.0, 128.0/255.0, 0.0, 1.0); \n"
+ "} \n";
+
+ vector<string> frag_shader_outputs;
+ program_num = resource_pool->compile_glsl_program(vert_shader, frag_shader, frag_shader_outputs);
+ check_error();
+
+ texture_sampler_uniform = glGetUniformLocation(program_num, "tex");
+ check_error();
+ position_attribute_index = glGetAttribLocation(program_num, "position");
+ check_error();
+ texcoord_attribute_index = glGetAttribLocation(program_num, "texcoord");
+ check_error();
+
+ // Shared between the two.
+ float vertices[] = {
+ 0.0f, 2.0f,
+ 0.0f, 0.0f,
+ 2.0f, 0.0f
+ };
+ vbo = generate_vbo(2, GL_FLOAT, sizeof(vertices), vertices);
+ check_error();
+
+ tex = resource_pool->create_2d_texture(GL_R8, display_width, height);
+
+ image.reset(new QImage(display_width, height, QImage::Format_Grayscale8));
+}
+
+TimecodeRenderer::~TimecodeRenderer()
+{
+ resource_pool->release_2d_texture(tex);
+ check_error();
+ resource_pool->release_glsl_program(program_num);
+ check_error();
+ glDeleteBuffers(1, &vbo);
+ check_error();
+}
+
+string TimecodeRenderer::get_timecode_text(double pts, unsigned frame_num)
+{
+ // Find the wall time, and round it to the nearest millisecond.
+ timeval now;
+ gettimeofday(&now, nullptr);
+ time_t unixtime = now.tv_sec;
+ unsigned msecs = (now.tv_usec + 500) / 1000;
+ if (msecs >= 1000) {
+ msecs -= 1000;
+ ++unixtime;
+ }
+
+ tm utc_tm;
+ gmtime_r(&unixtime, &utc_tm);
+ char clock_text[256];
+ strftime(clock_text, sizeof(clock_text), "%Y-%m-%d %H:%M:%S", &utc_tm);
+
+ // Make the stream timecode, rounded to the nearest millisecond.
+ long stream_time = lrint(pts * 1e3);
+ assert(stream_time >= 0);
+ unsigned stream_time_ms = stream_time % 1000;
+ stream_time /= 1000;
+ unsigned stream_time_sec = stream_time % 60;
+ stream_time /= 60;
+ unsigned stream_time_min = stream_time % 60;
+ unsigned stream_time_hour = stream_time / 60;
+
+ char timecode_text[256];
+ snprintf(timecode_text, sizeof(timecode_text), "Nageru - %s.%03u UTC - Stream time %02u:%02u:%02u.%03u (frame %u)",
+ clock_text, msecs, stream_time_hour, stream_time_min, stream_time_sec, stream_time_ms, frame_num);
+ return timecode_text;
+}
+
+void TimecodeRenderer::render_timecode(GLuint fbo, const string &text)
+{
+ render_string_to_buffer(text);
+ render_buffer_to_fbo(fbo);
+}
+
+void TimecodeRenderer::render_string_to_buffer(const string &text)
+{
+ image->fill(0);
+ QPainter painter(image.get());
+
+ painter.setPen(Qt::white);
+ QFont font = painter.font();
+ font.setPointSize(16);
+ painter.setFont(font);
+
+ painter.drawText(QRectF(0, 0, display_width, height), Qt::AlignCenter, QString::fromStdString(text));
+}
+
+void TimecodeRenderer::render_buffer_to_fbo(GLuint fbo)
+{
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ check_error();
+
+ GLuint vao;
+ glGenVertexArrays(1, &vao);
+ check_error();
+
+ glBindVertexArray(vao);
+ check_error();
+
+ glViewport(0, display_height - height, display_width, height);
+ check_error();
+
+ glActiveTexture(GL_TEXTURE0);
+ check_error();
+ glBindTexture(GL_TEXTURE_2D, tex);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ check_error();
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ check_error();
+
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, display_width, height, GL_RED, GL_UNSIGNED_BYTE, image->bits());
+ check_error();
+
+ glUseProgram(program_num);
+ check_error();
+ glUniform1i(texture_sampler_uniform, 0);
+ check_error();
+
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ check_error();
+
+ for (GLint attr_index : { position_attribute_index, texcoord_attribute_index }) {
+ if (attr_index == -1) continue;
+ glEnableVertexAttribArray(attr_index);
+ check_error();
+ glVertexAttribPointer(attr_index, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
+ check_error();
+ }
+
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ check_error();
+
+ for (GLint attr_index : { position_attribute_index, texcoord_attribute_index }) {
+ if (attr_index == -1) continue;
+ glDisableVertexAttribArray(attr_index);
+ check_error();
+ }
+
+ glActiveTexture(GL_TEXTURE0);
+ check_error();
+ glUseProgram(0);
+ check_error();
+
+ glDeleteVertexArrays(1, &vao);
+ check_error();
+
+ glBindFramebuffer(GL_FRAMEBUFFER, 0);
+ check_error();
+}
--- /dev/null
+#ifndef _TIMECODE_RENDERER_H
+#define _TIMECODE_RENDERER_H 1
+
+#include <memory>
+#include <string>
+
+#include <epoxy/gl.h>
+
+// A class to render a simple text string onto the picture using Qt and OpenGL.
+
+namespace movit {
+
+class ResourcePool;
+
+} // namespace movit
+
+class QImage;
+
+class TimecodeRenderer {
+public:
+ TimecodeRenderer(movit::ResourcePool *resource_pool, unsigned display_width, unsigned display_height);
+ ~TimecodeRenderer();
+
+ // Return a string with the current wall clock time and the
+ // logical stream time.
+ static std::string get_timecode_text(double pts, unsigned frame_num);
+
+ // The FBO is assumed to contain three outputs (Y', Cb/Cr and RGBA).
+ void render_timecode(GLuint fbo, const std::string &text);
+
+private:
+ void render_string_to_buffer(const std::string &text);
+ void render_buffer_to_fbo(GLuint fbo);
+
+ movit::ResourcePool *resource_pool;
+ unsigned display_width, display_height, height;
+
+ GLuint vbo; // Holds position and texcoord data.
+ GLuint tex;
+ //std::unique_ptr<unsigned char[]> pixel_buffer;
+ std::unique_ptr<QImage> image;
+
+ GLuint program_num; // Owned by <resource_pool>.
+ GLuint texture_sampler_uniform;
+ GLuint position_attribute_index, texcoord_attribute_index;
+};
+
+#endif
<rect>
<x>0</x>
<y>0</y>
- <width>514</width>
- <height>238</height>
+ <width>505</width>
+ <height>236</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<x>0</x>
<y>0</y>
<width>1089</width>
- <height>19</height>
+ <height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuWhat">
<property name="title">
<string>&Video</string>
</property>
+ <widget class="QMenu" name="display_timecode_menu">
+ <property name="title">
+ <string>Display &time code</string>
+ </property>
+ <addaction name="timecode_stream_action"/>
+ <addaction name="timecode_stdout_action"/>
+ </widget>
<addaction name="cut_action"/>
<addaction name="x264_bitrate_action"/>
+ <addaction name="display_timecode_menu"/>
<addaction name="exit_action"/>
</widget>
<widget class="QMenu" name="menu_Help">
<string>Online &manual…</string>
</property>
</action>
+ <action name="timecode_stream_action">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>In &stream</string>
+ </property>
+ </action>
+ <action name="timecode_stdout_action">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>On standard &output</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>