OBJS += quicksync_encoder.o x264_encoder.o x264_speed_control.o video_encoder.o metacube2.o mux.o audio_encoder.o ffmpeg_raii.o
# DeckLink
-OBJS += decklink_capture.o decklink_util.o decklink/DeckLinkAPIDispatch.o
+OBJS += decklink_capture.o decklink_util.o decklink_output.o decklink/DeckLinkAPIDispatch.o
# bmusb
ifeq ($(EMBEDDED_BMUSB),yes)
--- /dev/null
+#include <movit/effect_util.h>
+#include <movit/util.h>
+#include <movit/resource_pool.h> // Must be above the Xlib includes.
+
+#include <epoxy/egl.h>
+
+#include "chroma_subsampler.h"
+#include "decklink_output.h"
+#include "decklink_util.h"
+#include "flags.h"
+#include "print_latency.h"
+#include "resource_pool.h"
+#include "timebase.h"
+
+using namespace movit;
+using namespace std;
+using namespace std::chrono;
+
+DeckLinkOutput::DeckLinkOutput(ResourcePool *resource_pool, QSurface *surface, unsigned width, unsigned height, unsigned card_index)
+ : resource_pool(resource_pool), surface(surface), width(width), height(height), card_index(card_index)
+{
+ chroma_subsampler.reset(new ChromaSubsampler(resource_pool));
+}
+
+void DeckLinkOutput::set_device(IDeckLink *decklink)
+{
+ if (decklink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK) {
+ fprintf(stderr, "Card %u has no outputs\n", card_index);
+ exit(1);
+ }
+
+ IDeckLinkDisplayModeIterator *mode_it;
+ if (output->GetDisplayModeIterator(&mode_it) != S_OK) {
+ fprintf(stderr, "Failed to enumerate output display modes for card %u\n", card_index);
+ exit(1);
+ }
+
+ video_modes.clear();
+
+ for (const auto &it : summarize_video_modes(mode_it, card_index)) {
+ if (it.second.width != width || it.second.height != height) {
+ continue;
+ }
+
+ // We could support interlaced modes, but let's stay out of it for now,
+ // since we don't have interlaced stream output.
+ if (it.second.interlaced) {
+ continue;
+ }
+
+ video_modes.insert(it);
+ }
+
+ mode_it->Release();
+}
+
+void DeckLinkOutput::start_output(uint32_t mode, int64_t base_pts)
+{
+ assert(output);
+
+ should_quit = false;
+ playback_started = false;
+ this->base_pts = base_pts;
+
+ IDeckLinkConfiguration *config = nullptr;
+ if (output->QueryInterface(IID_IDeckLinkConfiguration, (void**)&config) != S_OK) {
+ fprintf(stderr, "Failed to get configuration interface for output card\n");
+ exit(1);
+ }
+ if (config->SetFlag(bmdDeckLinkConfigLowLatencyVideoOutput, true) != S_OK) {
+ fprintf(stderr, "Failed to set low latency output\n");
+ exit(1);
+ }
+ // HDMI or SDI generally mean “both HDMI and SDI at the same time” on DeckLink cards.
+ // We're not very likely to need analog outputs.
+ if (config->SetInt(bmdDeckLinkConfigVideoOutputConnection, bmdVideoConnectionHDMI) != S_OK) {
+ fprintf(stderr, "Failed to set video output connection for card %u\n", card_index);
+ exit(1);
+ }
+ if (config->SetFlag(bmdDeckLinkConfigUse1080pNotPsF, true) != S_OK) {
+ fprintf(stderr, "Failed to set PsF flag for card\n");
+ exit(1);
+ }
+
+ BMDDisplayModeSupport support;
+ IDeckLinkDisplayMode *display_mode;
+ if (output->DoesSupportVideoMode(mode, bmdFormat8BitYUV, bmdVideoOutputFlagDefault,
+ &support, &display_mode) != S_OK) {
+ fprintf(stderr, "Couldn't ask for format support\n");
+ exit(1);
+ }
+
+ if (support == bmdDisplayModeNotSupported) {
+ fprintf(stderr, "Requested display mode not supported\n");
+ exit(1);
+ }
+
+ BMDDisplayModeFlags flags = display_mode->GetFlags();
+ if ((flags & bmdDisplayModeColorspaceRec601) && global_flags.ycbcr_rec709_coefficients) {
+ fprintf(stderr, "WARNING: Chosen output mode expects Rec. 601 Y'CbCr coefficients.\n");
+ fprintf(stderr, " Consider --output-ycbcr-coefficients=rec601 (or =auto).\n");
+ } else if ((flags & bmdDisplayModeColorspaceRec709) && !global_flags.ycbcr_rec709_coefficients) {
+ fprintf(stderr, "WARNING: Chosen output mode expects Rec. 709 Y'CbCr coefficients.\n");
+ fprintf(stderr, " Consider --output-ycbcr-coefficients=rec709 (or =auto).\n");
+ }
+
+ BMDTimeValue time_value;
+ BMDTimeScale time_scale;
+ if (display_mode->GetFrameRate(&time_value, &time_scale) != S_OK) {
+ fprintf(stderr, "Couldn't get frame rate\n");
+ exit(1);
+ }
+
+ frame_duration = time_value * TIMEBASE / time_scale;
+
+ display_mode->Release();
+
+ HRESULT result = output->EnableVideoOutput(mode, bmdVideoOutputFlagDefault);
+ if (result != S_OK) {
+ fprintf(stderr, "Couldn't enable output with error 0x%x\n", result);
+ exit(1);
+ }
+ if (output->SetScheduledFrameCompletionCallback(this) != S_OK) {
+ fprintf(stderr, "Couldn't set callback\n");
+ exit(1);
+ }
+ assert(OUTPUT_FREQUENCY == 48000);
+ if (output->EnableAudioOutput(bmdAudioSampleRate48kHz, bmdAudioSampleType32bitInteger, 2, bmdAudioOutputStreamTimestamped) != S_OK) {
+ fprintf(stderr, "Couldn't enable audio output\n");
+ exit(1);
+ }
+ if (output->BeginAudioPreroll() != S_OK) {
+ fprintf(stderr, "Couldn't begin audio preroll\n");
+ exit(1);
+ }
+
+ present_thread = thread([this]{
+ QOpenGLContext *context = create_context(this->surface);
+ eglBindAPI(EGL_OPENGL_API);
+ if (!make_current(context, this->surface)) {
+ printf("display=%p surface=%p context=%p curr=%p err=%d\n", eglGetCurrentDisplay(), this->surface, context, eglGetCurrentContext(),
+ eglGetError());
+ exit(1);
+ }
+ present_thread_func();
+ delete_context(context);
+ });
+}
+
+void DeckLinkOutput::end_output()
+{
+ should_quit = true;
+ frame_queues_changed.notify_all();
+ present_thread.join();
+
+ output->StopScheduledPlayback(0, nullptr, 0);
+ output->DisableVideoOutput();
+ output->DisableAudioOutput();
+
+ // Wait until all frames are accounted for, and free them.
+ {
+ unique_lock<mutex> lock(frame_queue_mutex);
+ while (!(frame_freelist.empty() && num_frames_in_flight == 0)) {
+ frame_queues_changed.wait(lock, [this]{ return !frame_freelist.empty(); });
+ frame_freelist.pop();
+ }
+ }
+}
+
+void DeckLinkOutput::send_frame(GLuint y_tex, GLuint cbcr_tex, const vector<RefCountedFrame> &input_frames, int64_t pts, int64_t duration)
+{
+ unique_ptr<Frame> frame = move(get_frame());
+ chroma_subsampler->create_uyvy(y_tex, cbcr_tex, width, height, frame->uyvy_tex);
+
+ // Download the UYVY texture to the PBO.
+ glPixelStorei(GL_PACK_ROW_LENGTH, 0);
+ check_error();
+
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, frame->pbo);
+ check_error();
+
+ glBindTexture(GL_TEXTURE_2D, frame->uyvy_tex);
+ check_error();
+ glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, BUFFER_OFFSET(0));
+ check_error();
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ check_error();
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+ check_error();
+
+ glMemoryBarrier(GL_TEXTURE_UPDATE_BARRIER_BIT | GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
+ check_error();
+
+ frame->fence = RefCountedGLsync(GL_SYNC_GPU_COMMANDS_COMPLETE, /*flags=*/0);
+ check_error();
+ glFlush(); // Make the DeckLink thread see the fence as soon as possible.
+ check_error();
+
+ frame->input_frames = input_frames;
+ frame->received_ts = find_received_timestamp(input_frames);
+ frame->pts = pts;
+ frame->duration = duration;
+
+ {
+ unique_lock<mutex> lock(frame_queue_mutex);
+ pending_video_frames.push(move(frame));
+ }
+ frame_queues_changed.notify_all();
+}
+
+void DeckLinkOutput::send_audio(int64_t pts, const std::vector<float> &samples)
+{
+ unique_ptr<int32_t[]> int_samples(new int32_t[samples.size()]);
+ for (size_t i = 0; i < samples.size(); ++i) {
+ int_samples[i] = lrintf(samples[i] * 2147483648.0f);
+ }
+
+ uint32_t frames_written;
+ HRESULT result = output->ScheduleAudioSamples(int_samples.get(), samples.size() / 2,
+ pts, TIMEBASE, &frames_written);
+ if (result != S_OK) {
+ fprintf(stderr, "ScheduleAudioSamples(pts=%ld) failed (result=0x%08x)\n", pts, result);
+ } else {
+ if (frames_written != samples.size() / 2) {
+ fprintf(stderr, "ScheduleAudioSamples() returned short write (%u/%ld)\n", frames_written, samples.size() / 2);
+ }
+ }
+}
+
+void DeckLinkOutput::wait_for_frame(int64_t pts, int *dropped_frames, int64_t *frame_duration)
+{
+ *dropped_frames = 0;
+ *frame_duration = this->frame_duration;
+
+ const BMDTimeValue buffer = lrint(*frame_duration * global_flags.output_buffer_frames);
+ const BMDTimeValue max_overshoot = lrint(*frame_duration * global_flags.output_slop_frames);
+ BMDTimeValue target_time = pts - buffer;
+
+ // While prerolling, we send out frames as quickly as we can.
+ if (target_time < base_pts) {
+ return;
+ }
+
+ if (!playback_started) {
+ if (output->EndAudioPreroll() != S_OK) {
+ fprintf(stderr, "Could not end audio preroll\n");
+ exit(1); // TODO
+ }
+ if (output->StartScheduledPlayback(base_pts, TIMEBASE, 1.0) != S_OK) {
+ fprintf(stderr, "Could not start playback\n");
+ exit(1); // TODO
+ }
+ playback_started = true;
+ }
+
+ BMDTimeValue stream_frame_time;
+ double playback_speed;
+ output->GetScheduledStreamTime(TIMEBASE, &stream_frame_time, &playback_speed);
+
+ // If we're ahead of time, wait for the frame to (approximately) start.
+ if (stream_frame_time < target_time) {
+ steady_clock::time_point t = steady_clock::now() +
+ nanoseconds((target_time - stream_frame_time) * 1000000000 / TIMEBASE);
+ this_thread::sleep_until(t);
+ return;
+ }
+
+ // If we overshot the previous frame by just a little,
+ // fire off one immediately.
+ if (stream_frame_time < target_time + max_overshoot) {
+ fprintf(stderr, "Warning: Frame was %ld ms late (but not skipping it due to --output-slop-frames).\n",
+ lrint((stream_frame_time - target_time) * 1000.0 / TIMEBASE));
+ return;
+ }
+
+ // Oops, we missed by more than one frame. Return immediately,
+ // but drop so that we catch up.
+ *dropped_frames = (stream_frame_time - target_time + *frame_duration - 1) / *frame_duration;
+ fprintf(stderr, "Dropped %d output frames; skipping.\n", *dropped_frames);
+}
+
+HRESULT DeckLinkOutput::ScheduledFrameCompleted(/* in */ IDeckLinkVideoFrame *completedFrame, /* in */ BMDOutputFrameCompletionResult result)
+{
+ Frame *frame = static_cast<Frame *>(completedFrame);
+ switch (result) {
+ case bmdOutputFrameCompleted:
+ break;
+ case bmdOutputFrameDisplayedLate:
+ fprintf(stderr, "Output frame displayed late (pts=%ld)\n", frame->pts);
+ fprintf(stderr, "Consider increasing --output-buffer-frames if this persists.\n");
+ break;
+ case bmdOutputFrameDropped:
+ fprintf(stderr, "Output frame was dropped (pts=%ld)\n", frame->pts);
+ fprintf(stderr, "Consider increasing --output-buffer-frames if this persists.\n");
+ break;
+ case bmdOutputFrameFlushed:
+ fprintf(stderr, "Output frame was flushed (pts=%ld)\n", frame->pts);
+ break;
+ default:
+ fprintf(stderr, "Output frame completed with unknown status %d\n", result);
+ break;
+ }
+
+ static int hei = 0;
+ print_latency("DeckLink output latency (frame received → output on HDMI):", frame->received_ts, false, &hei);
+
+ {
+ lock_guard<mutex> lock(frame_queue_mutex);
+ frame_freelist.push(unique_ptr<Frame>(frame));
+ --num_frames_in_flight;
+ }
+
+ return S_OK;
+}
+
+HRESULT DeckLinkOutput::ScheduledPlaybackHasStopped()
+{
+ printf("playback stopped!\n");
+ return S_OK;
+}
+
+unique_ptr<DeckLinkOutput::Frame> DeckLinkOutput::get_frame()
+{
+ lock_guard<mutex> lock(frame_queue_mutex);
+
+ if (!frame_freelist.empty()) {
+ unique_ptr<Frame> frame = move(frame_freelist.front());
+ frame_freelist.pop();
+ return frame;
+ }
+
+ unique_ptr<Frame> frame(new Frame);
+
+ frame->uyvy_tex = resource_pool->create_2d_texture(GL_RGBA8, width / 2, height);
+
+ glGenBuffers(1, &frame->pbo);
+ check_error();
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, frame->pbo);
+ check_error();
+ glBufferStorage(GL_PIXEL_PACK_BUFFER, width * height * 2, NULL, GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT);
+ check_error();
+ frame->uyvy_ptr = (uint8_t *)glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, width * height * 2, GL_MAP_READ_BIT | GL_MAP_PERSISTENT_BIT);
+ check_error();
+ frame->uyvy_ptr_local.reset(new uint8_t[width * height * 2]);
+ frame->resource_pool = resource_pool;
+
+ return frame;
+}
+
+void DeckLinkOutput::present_thread_func()
+{
+ for ( ;; ) {
+ unique_ptr<Frame> frame;
+ {
+ unique_lock<mutex> lock(frame_queue_mutex);
+ frame_queues_changed.wait(lock, [this]{
+ return should_quit || !pending_video_frames.empty();
+ });
+ if (should_quit) {
+ return;
+ }
+ frame = move(pending_video_frames.front());
+ pending_video_frames.pop();
+ ++num_frames_in_flight;
+ }
+
+ glWaitSync(frame->fence.get(), /*flags=*/0, GL_TIMEOUT_IGNORED);
+ check_error();
+ frame->fence.reset();
+
+ memcpy(frame->uyvy_ptr_local.get(), frame->uyvy_ptr, width * height * 2);
+
+ // Release any input frames we needed to render this frame.
+ frame->input_frames.clear();
+
+ BMDTimeValue pts = frame->pts;
+ BMDTimeValue duration = frame->duration;
+ HRESULT res = output->ScheduleVideoFrame(frame.get(), pts, duration, TIMEBASE);
+ if (res == S_OK) {
+ frame.release(); // Owned by the driver now.
+ } else {
+ fprintf(stderr, "Could not schedule video frame! (error=0x%08x)\n", res);
+
+ lock_guard<mutex> lock(frame_queue_mutex);
+ frame_freelist.push(move(frame));
+ --num_frames_in_flight;
+ }
+ }
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkOutput::QueryInterface(REFIID, LPVOID *)
+{
+ return E_NOINTERFACE;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkOutput::AddRef()
+{
+ return refcount.fetch_add(1) + 1;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkOutput::Release()
+{
+ int new_ref = refcount.fetch_sub(1) - 1;
+ if (new_ref == 0)
+ delete this;
+ return new_ref;
+}
+
+DeckLinkOutput::Frame::~Frame()
+{
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+ check_error();
+ glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+ check_error();
+ glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+ check_error();
+ glDeleteBuffers(1, &pbo);
+ check_error();
+ resource_pool->release_2d_texture(uyvy_tex);
+ check_error();
+}
+
+HRESULT STDMETHODCALLTYPE DeckLinkOutput::Frame::QueryInterface(REFIID, LPVOID *)
+{
+ return E_NOINTERFACE;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkOutput::Frame::AddRef()
+{
+ return refcount.fetch_add(1) + 1;
+}
+
+ULONG STDMETHODCALLTYPE DeckLinkOutput::Frame::Release()
+{
+ int new_ref = refcount.fetch_sub(1) - 1;
+ if (new_ref == 0)
+ delete this;
+ return new_ref;
+}
+
+long DeckLinkOutput::Frame::GetWidth()
+{
+ return global_flags.width;
+}
+
+long DeckLinkOutput::Frame::GetHeight()
+{
+ return global_flags.height;
+}
+
+long DeckLinkOutput::Frame::GetRowBytes()
+{
+ return global_flags.width * 2;
+}
+
+BMDPixelFormat DeckLinkOutput::Frame::GetPixelFormat()
+{
+ return bmdFormat8BitYUV;
+}
+
+BMDFrameFlags DeckLinkOutput::Frame::GetFlags()
+{
+ return bmdFrameFlagDefault;
+}
+
+HRESULT DeckLinkOutput::Frame::GetBytes(/* out */ void **buffer)
+{
+ *buffer = uyvy_ptr_local.get();
+ return S_OK;
+}
+
+HRESULT DeckLinkOutput::Frame::GetTimecode(/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode **timecode)
+{
+ fprintf(stderr, "STUB: GetTimecode()\n");
+ return E_NOTIMPL;
+}
+
+HRESULT DeckLinkOutput::Frame::GetAncillaryData(/* out */ IDeckLinkVideoFrameAncillary **ancillary)
+{
+ fprintf(stderr, "STUB: GetAncillaryData()\n");
+ return E_NOTIMPL;
+}
--- /dev/null
+#ifndef _DECKLINK_OUTPUT_H
+#define _DECKLINK_OUTPUT_H 1
+
+#include <epoxy/gl.h>
+#include <stdint.h>
+#include <atomic>
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <queue>
+#include <thread>
+#include <vector>
+
+#include "DeckLinkAPI.h"
+#include "DeckLinkAPITypes.h"
+#include "LinuxCOM.h"
+
+#include "context.h"
+#include "print_latency.h"
+#include "ref_counted_frame.h"
+#include "ref_counted_gl_sync.h"
+
+namespace movit {
+
+class ResourcePool;
+
+} // namespace movit
+
+class ChromaSubsampler;
+class IDeckLink;
+class IDeckLinkOutput;
+class QSurface;
+
+class DeckLinkOutput : public IDeckLinkVideoOutputCallback {
+public:
+ DeckLinkOutput(movit::ResourcePool *resource_pool, QSurface *surface, unsigned width, unsigned height, unsigned card_index);
+
+ void set_device(IDeckLink *output);
+ void start_output(uint32_t mode, int64_t base_pts); // Mode comes from get_available_video_modes().
+ void end_output();
+
+ void send_frame(GLuint y_tex, GLuint cbcr_tex, const std::vector<RefCountedFrame> &input_frames, int64_t pts, int64_t duration);
+ void send_audio(int64_t pts, const std::vector<float> &samples);
+ void wait_for_frame(int64_t pts, int *dropped_frames, int64_t *frame_duration);
+
+ // Analogous to CaptureInterface. Will only return modes that have the right width/height.
+ std::map<uint32_t, bmusb::VideoMode> get_available_video_modes() const { return video_modes; }
+
+ // IUnknown.
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+
+ // IDeckLinkVideoOutputCallback.
+ HRESULT ScheduledFrameCompleted(/* in */ IDeckLinkVideoFrame *completedFrame, /* in */ BMDOutputFrameCompletionResult result) override;
+ HRESULT ScheduledPlaybackHasStopped() override;
+
+private:
+ struct Frame : public IDeckLinkVideoFrame {
+ public:
+ ~Frame();
+
+ // IUnknown.
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
+ ULONG STDMETHODCALLTYPE AddRef() override;
+ ULONG STDMETHODCALLTYPE Release() override;
+
+ // IDeckLinkVideoFrame.
+ long GetWidth() override;
+ long GetHeight() override;
+ long GetRowBytes() override;
+ BMDPixelFormat GetPixelFormat() override;
+ BMDFrameFlags GetFlags() override;
+ HRESULT GetBytes(/* out */ void **buffer) override;
+
+ HRESULT GetTimecode(/* in */ BMDTimecodeFormat format, /* out */ IDeckLinkTimecode **timecode) override;
+ HRESULT GetAncillaryData(/* out */ IDeckLinkVideoFrameAncillary **ancillary) override;
+
+ private:
+ std::atomic<int> refcount{1};
+ RefCountedGLsync fence; // Needs to be waited on before uyvy_ptr can be read from.
+ std::vector<RefCountedFrame> input_frames; // Cannot be released before we are done rendering (ie., <fence> is asserted).
+ ReceivedTimestamps received_ts;
+ int64_t pts, duration;
+ movit::ResourcePool *resource_pool;
+
+ // These members are persistently allocated, and reused when the frame object is.
+ GLuint uyvy_tex; // Owned by <resource_pool>.
+ GLuint pbo;
+ uint8_t *uyvy_ptr; // Persistent mapping into the PBO.
+
+ // Current Blackmagic drivers (January 2017) have a bug where sending a PBO
+ // pointer to the driver causes a kernel oops. Thus, we do an extra copy into
+ // this pointer before giving the data to the driver. (We don't do a get
+ // directly into this pointer, because e.g. Intel/Mesa hits a slow path when
+ // you do readback into something that's not a PBO.) When Blackmagic fixes
+ // the bug, we should drop this.
+ std::unique_ptr<uint8_t[]> uyvy_ptr_local;
+
+ friend class DeckLinkOutput;
+ };
+ std::unique_ptr<Frame> get_frame();
+ void create_uyvy(GLuint y_tex, GLuint cbcr_tex, GLuint dst_tex);
+
+ void present_thread_func();
+
+ std::atomic<int> refcount{1};
+
+ std::unique_ptr<ChromaSubsampler> chroma_subsampler;
+ std::map<uint32_t, bmusb::VideoMode> video_modes;
+
+ std::thread present_thread;
+ std::atomic<bool> should_quit{false};
+
+ std::mutex frame_queue_mutex;
+ std::queue<std::unique_ptr<Frame>> pending_video_frames; // Under <frame_queue_mutex>.
+ std::queue<std::unique_ptr<Frame>> frame_freelist; // Under <frame_queue_mutex>.
+ int num_frames_in_flight = 0; // Number of frames allocated but not on the freelist. Under <frame_queue_mutex>.
+ std::condition_variable frame_queues_changed;
+ bool playback_started = false;
+ int64_t base_pts, frame_duration;
+
+ movit::ResourcePool *resource_pool;
+ IDeckLinkOutput *output = nullptr;
+ QSurface *surface;
+ unsigned width, height;
+ unsigned card_index;
+
+ GLuint uyvy_vbo; // Holds position and texcoord data.
+ GLuint uyvy_program_num; // Owned by <resource_pool>.
+ GLuint uyvy_position_attribute_index, uyvy_texcoord_attribute_index;
+};
+
+#endif // !defined(_DECKLINK_OUTPUT_H)
OPTION_NO_FLUSH_PBOS,
OPTION_PRINT_VIDEO_LATENCY,
OPTION_AUDIO_QUEUE_LENGTH_MS,
- OPTION_OUTPUT_YCBCR_COEFFICIENTS
+ OPTION_OUTPUT_YCBCR_COEFFICIENTS,
+ OUTPUT_BUFFER_FRAMES,
+ OUTPUT_SLOP_FRAMES,
};
void usage()
fprintf(stderr, " -w, --width output width in pixels (default 1280)\n");
fprintf(stderr, " -h, --height output height in pixels (default 720)\n");
fprintf(stderr, " -c, --num-cards set number of input cards (default 2)\n");
+ fprintf(stderr, " -o, --output-card=CARD also output signal to the given card (default none)\n");
fprintf(stderr, " -t, --theme=FILE choose theme (default theme.lua)\n");
fprintf(stderr, " -I, --theme-dir=DIR search for theme in this directory (can be given multiple times)\n");
fprintf(stderr, " -v, --va-display=SPEC VA-API device for H.264 encoding\n");
fprintf(stderr, " (will give display corruption, but makes it\n");
fprintf(stderr, " possible to run with apitrace in real time)\n");
fprintf(stderr, " --print-video-latency print out measurements of video latency on stdout\n");
- fprintf(stderr, " --audio-queue-length-ms length of audio resampling queue (default 100.0)\n");
- fprintf(stderr, " --output-ycbcr-coefficients={rec601,rec709}\n");
- fprintf(stderr, " Y'CbCr coefficient standard of output (default rec601)\n");
+ fprintf(stderr, " --audio-queue-length-ms=MS length of audio resampling queue (default 100.0)\n");
+ fprintf(stderr, " --output-ycbcr-coefficients={rec601,rec709,auto}\n");
+ fprintf(stderr, " Y'CbCr coefficient standard of output (default auto)\n");
+ fprintf(stderr, " auto is rec709 if and only if --output-card is used\n");
+ fprintf(stderr, " and a HD resolution is set\n");
+ fprintf(stderr, " --output-buffer-frames=NUM number of frames in output buffer for --output-card,\n");
+ fprintf(stderr, " can be fractional (default 6.0); note also\n");
+ fprintf(stderr, " the audio queue can't be much longer than this\n");
+ 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");
}
void parse_flags(int argc, char * const argv[])
{ "width", required_argument, 0, 'w' },
{ "height", required_argument, 0, 'h' },
{ "num-cards", required_argument, 0, 'c' },
+ { "output-card", required_argument, 0, 'o' },
{ "theme", required_argument, 0, 't' },
{ "theme-dir", required_argument, 0, 'I' },
{ "map-signal", required_argument, 0, 'm' },
{ "print-video-latency", no_argument, 0, OPTION_PRINT_VIDEO_LATENCY },
{ "audio-queue-length-ms", required_argument, 0, OPTION_AUDIO_QUEUE_LENGTH_MS },
{ "output-ycbcr-coefficients", required_argument, 0, OPTION_OUTPUT_YCBCR_COEFFICIENTS },
+ { "output-buffer-frames", required_argument, 0, OUTPUT_BUFFER_FRAMES },
+ { "output-slop-frames", required_argument, 0, OUTPUT_SLOP_FRAMES },
{ 0, 0, 0, 0 }
};
vector<string> theme_dirs;
- string output_ycbcr_coefficients = "rec601";
+ string output_ycbcr_coefficients = "auto";
for ( ;; ) {
int option_index = 0;
int c = getopt_long(argc, argv, "c:t:I:v:m:M:w:h:", long_options, &option_index);
case 'c':
global_flags.num_cards = atoi(optarg);
break;
+ case 'o':
+ global_flags.output_card = atoi(optarg);
+ break;
case 't':
global_flags.theme_filename = optarg;
break;
case OPTION_OUTPUT_YCBCR_COEFFICIENTS:
output_ycbcr_coefficients = optarg;
break;
+ case OUTPUT_BUFFER_FRAMES:
+ global_flags.output_buffer_frames = atof(optarg);
+ break;
+ case OUTPUT_SLOP_FRAMES:
+ global_flags.output_slop_frames = atof(optarg);
+ break;
case OPTION_HELP:
usage();
exit(0);
fprintf(stderr, "ERROR: --num-cards must be at least 1\n");
exit(1);
}
+ if (global_flags.output_card < -1 ||
+ global_flags.output_card >= global_flags.num_cards) {
+ fprintf(stderr, "ERROR: --output-card points to a nonexistant card\n");
+ exit(1);
+ }
if (global_flags.x264_speedcontrol) {
if (!global_flags.x264_preset.empty() && global_flags.x264_preset != "faster") {
fprintf(stderr, "WARNING: --x264-preset is overridden by --x264-speedcontrol (implicitly uses \"faster\" as base preset)\n");
}
}
- if (output_ycbcr_coefficients == "rec709") {
+ // Rec. 709 would be the sane thing to do, but it seems many players
+ // just default to BT.601 coefficients no matter what. We _do_ set
+ // the right flags, so that a player that works properly doesn't have
+ // to guess, but it's frequently ignored. See discussions
+ // in e.g. https://trac.ffmpeg.org/ticket/4978; the situation with
+ // browsers is complicated and depends on things like hardware acceleration
+ // (https://bugs.chromium.org/p/chromium/issues/detail?id=333619 for
+ // extensive discussion). VLC generally fixed this as part of 3.0.0
+ // (see e.g. https://github.com/videolan/vlc/commit/bc71288b2e38c07d6921472824b92eef1aa85f7e
+ // and https://github.com/videolan/vlc/commit/c3fc2683a9cde1d42674ebf9935dced05733a215),
+ // but earlier versions were pretty random.
+ //
+ // On the other hand, HDMI/SDI output typically requires Rec. 709 for
+ // HD resolutions (with no way of signaling anything else), which is
+ // a conflicting demand. In this case, we typically let the HDMI/SDI
+ // output win, but the user can override this.
+ if (output_ycbcr_coefficients == "auto") {
+ if (global_flags.output_card >= 0 && global_flags.width >= 1280) {
+ global_flags.ycbcr_rec709_coefficients = true;
+ } else {
+ global_flags.ycbcr_rec709_coefficients = false;
+ }
+ } else if (output_ycbcr_coefficients == "rec709") {
global_flags.ycbcr_rec709_coefficients = true;
} else if (output_ycbcr_coefficients == "rec601") {
global_flags.ycbcr_rec709_coefficients = false;
} else {
- fprintf(stderr, "ERROR: --output-ycbcr-coefficients must be “rec601” or “rec709”\n");
+ fprintf(stderr, "ERROR: --output-ycbcr-coefficients must be “rec601”, “rec709” or “auto”\n");
+ exit(1);
+ }
+
+ if (global_flags.output_buffer_frames < 0.0f) {
+ // Actually, even zero probably won't make sense; there is some internal
+ // delay to the card.
+ fprintf(stderr, "ERROR: --output-buffer-frames can't be negative.\n");
+ exit(1);
+ }
+ if (global_flags.output_slop_frames < 0.0f) {
+ fprintf(stderr, "ERROR: --output-slop-frames can't be negative.\n");
exit(1);
}
}
bool print_video_latency = false;
double audio_queue_length_ms = 100.0;
bool ycbcr_rec709_coefficients = false;
+ int output_card = -1;
+ double output_buffer_frames = 6.0;
+ double output_slop_frames = 0.5;
};
extern Flags global_flags;
#include "chroma_subsampler.h"
#include "context.h"
#include "decklink_capture.h"
+#include "decklink_output.h"
#include "defs.h"
#include "disk_space_estimator.h"
#include "flags.h"
num_cards(num_cards),
mixer_surface(create_surface(format)),
h264_encoder_surface(create_surface(format)),
+ decklink_output_surface(create_surface(format)),
audio_mixer(num_cards)
{
CHECK(init_movit(MOVIT_SHADER_DIR, MOVIT_DEBUG_OFF));
break;
}
- configure_card(card_index, new DeckLinkCapture(decklink, card_index), /*is_fake_capture=*/false);
+ DeckLinkCapture *capture = new DeckLinkCapture(decklink, card_index);
+ DeckLinkOutput *output = new DeckLinkOutput(resource_pool.get(), decklink_output_surface, global_flags.width, global_flags.height, card_index);
+ output->set_device(decklink);
+ configure_card(card_index, capture, /*is_fake_capture=*/false, output);
++num_pci_devices;
}
decklink_iterator->Release();
fprintf(stderr, "DeckLink drivers not found. Probing for USB cards only.\n");
}
}
+
unsigned num_usb_devices = BMUSBCapture::num_cards();
for (unsigned usb_card_index = 0; usb_card_index < num_usb_devices && card_index < num_cards; ++usb_card_index, ++card_index) {
BMUSBCapture *capture = new BMUSBCapture(usb_card_index);
capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, card_index));
- configure_card(card_index, capture, /*is_fake_capture=*/false);
+ configure_card(card_index, capture, /*is_fake_capture=*/false, /*output=*/nullptr);
}
fprintf(stderr, "Found %u USB card(s).\n", num_usb_devices);
unsigned num_fake_cards = 0;
for ( ; card_index < num_cards; ++card_index, ++num_fake_cards) {
FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
- configure_card(card_index, capture, /*is_fake_capture=*/true);
+ configure_card(card_index, capture, /*is_fake_capture=*/true, /*output=*/nullptr);
}
if (num_fake_cards > 0) {
if (global_flags.enable_alsa_output) {
alsa.reset(new ALSAOutput(OUTPUT_FREQUENCY, /*num_channels=*/2));
}
+ if (global_flags.output_card != -1) {
+ set_output_card(global_flags.output_card);
+ }
}
Mixer::~Mixer()
video_encoder.reset(nullptr);
}
-void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, bool is_fake_capture)
+void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, bool is_fake_capture, DeckLinkOutput *output)
{
printf("Configuring card %d...\n", card_index);
}
card->capture = capture;
card->is_fake_capture = is_fake_capture;
+ card->output = output;
card->capture->set_frame_callback(bind(&Mixer::bm_frame, this, card_index, _1, _2, _3, _4, _5, _6, _7));
if (card->frame_allocator == nullptr) {
card->frame_allocator.reset(new PBOFrameAllocator(8 << 20, global_flags.width, global_flags.height)); // 8 MB.
audio_mixer.trigger_state_changed_callback();
}
+void Mixer::set_output_card(int card_index)
+{
+ if (card_index == output_card_index) {
+ return;
+ }
+ unique_lock<mutex> lock(card_mutex);
+ if (output_card_index != -1) {
+ // Switch the old card from output to input.
+ CaptureCard *old_card = &cards[output_card_index];
+ old_card->output->end_output();
+
+ old_card->capture->stop_dequeue_thread();
+ delete old_card->capture;
+
+ old_card->capture = old_card->parked_capture;
+ old_card->is_fake_capture = false;
+ old_card->parked_capture = nullptr;
+ old_card->capture->start_bm_capture();
+ }
+
+ CaptureCard *card = &cards[card_index];
+ card->capture->stop_dequeue_thread();
+ card->parked_capture = card->capture;
+ FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
+ configure_card(card_index, capture, /*is_fake_capture=*/true, card->output);
+ card->queue_length_policy.reset(card_index);
+ card->capture->start_bm_capture();
+ card->output->start_output(bmdModeHD720p5994, pts_int); // FIXME
+ output_card_index = card_index;
+}
namespace {
// Start the actual capture. (We don't want to do it before we're actually ready
// to process output frames.)
for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
- cards[card_index].capture->start_bm_capture();
+ if (int(card_index) != output_card_index) {
+ cards[card_index].capture->start_bm_capture();
+ }
}
steady_clock::time_point start, now;
CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS];
bool has_new_frame[MAX_VIDEO_CARDS] = { false };
- unsigned master_card_index = theme->map_signal(master_clock_channel);
- assert(master_card_index < num_cards);
+ bool master_card_is_output;
+ unsigned master_card_index;
+ if (output_card_index != -1) {
+ master_card_is_output = true;
+ master_card_index = output_card_index;
+ } else {
+ master_card_is_output = false;
+ master_card_index = theme->map_signal(master_clock_channel);
+ assert(master_card_index < num_cards);
+ }
- OutputFrameInfo output_frame_info = get_one_frame_from_each_card(master_card_index, new_frames, has_new_frame);
+ OutputFrameInfo output_frame_info = get_one_frame_from_each_card(master_card_index, master_card_is_output, new_frames, has_new_frame);
schedule_audio_resampling_tasks(output_frame_info.dropped_frames, output_frame_info.num_samples, output_frame_info.frame_duration);
stats_dropped_frames += output_frame_info.dropped_frames;
// If the first card is reporting a corrupted or otherwise dropped frame,
// just increase the pts (skipping over this frame) and don't try to compute anything new.
- if (new_frames[master_card_index].frame->len == 0) {
+ if (!master_card_is_output && new_frames[master_card_index].frame->len == 0) {
++stats_dropped_frames;
pts_int += new_frames[master_card_index].length;
continue;
resource_pool->clean_context();
}
-Mixer::OutputFrameInfo Mixer::get_one_frame_from_each_card(unsigned master_card_index, CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS], bool has_new_frame[MAX_VIDEO_CARDS])
+bool Mixer::input_card_is_master_clock(unsigned card_index, unsigned master_card_index) const
{
- OutputFrameInfo output_frame_info;
+ if (output_card_index != -1) {
+ // The output card (ie., cards[output_card_index].output) is the master clock,
+ // so no input card (ie., cards[card_index].capture) is.
+ return false;
+ }
+ return (card_index == master_card_index);
+}
+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])
+{
+ OutputFrameInfo output_frame_info;
start:
- // The first card is the master timer, so wait for it to have a new frame.
- // TODO: Add a timeout.
- unique_lock<mutex> lock(card_mutex);
- cards[master_card_index].new_frames_changed.wait(lock, [this, master_card_index]{ return !cards[master_card_index].new_frames.empty() || cards[master_card_index].capture->get_disconnected(); });
+ unique_lock<mutex> lock(card_mutex, defer_lock);
+ if (master_card_is_output) {
+ // Clocked to the output, so wait for it to be ready for the next frame.
+ cards[master_card_index].output->wait_for_frame(pts_int, &output_frame_info.dropped_frames, &output_frame_info.frame_duration);
+ lock.lock();
+ } else {
+ // Wait for the master card to have a new frame.
+ // TODO: Add a timeout.
+ lock.lock();
+ cards[master_card_index].new_frames_changed.wait(lock, [this, master_card_index]{ return !cards[master_card_index].new_frames.empty() || cards[master_card_index].capture->get_disconnected(); });
+ }
- if (cards[master_card_index].new_frames.empty()) {
+ if (master_card_is_output) {
+ handle_hotplugged_cards();
+ } else if (cards[master_card_index].new_frames.empty()) {
// We were woken up, but not due to a new frame. Deal with it
// and then restart.
assert(cards[master_card_index].capture->get_disconnected());
for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
CaptureCard *card = &cards[card_index];
if (card->new_frames.empty()) {
- assert(card_index != master_card_index);
+ assert(!input_card_is_master_clock(card_index, master_card_index));
card->queue_length_policy.update_policy(-1);
continue;
}
card->new_frames.pop();
card->new_frames_changed.notify_all();
- if (card_index == master_card_index) {
+ if (input_card_is_master_clock(card_index, master_card_index)) {
// We don't use the queue length policy for the master card,
// but we will if it stops being the master. Thus, clear out
// the policy in case we switch in the future.
}
}
- output_frame_info.dropped_frames = new_frames[master_card_index].dropped_frames;
- output_frame_info.frame_duration = new_frames[master_card_index].length;
+ if (!master_card_is_output) {
+ output_frame_info.dropped_frames = new_frames[master_card_index].dropped_frames;
+ output_frame_info.frame_duration = new_frames[master_card_index].length;
+ }
// This might get off by a fractional sample when changing master card
// between ones with different frame rates, but that's fine.
if (card->capture->get_disconnected()) {
fprintf(stderr, "Card %u went away, replacing with a fake card.\n", card_index);
FakeCapture *capture = new FakeCapture(global_flags.width, global_flags.height, FAKE_FPS, OUTPUT_FREQUENCY, card_index, global_flags.fake_cards_audio);
- configure_card(card_index, capture, /*is_fake_capture=*/true);
+ configure_card(card_index, capture, /*is_fake_capture=*/true, /*output=*/nullptr);
card->queue_length_policy.reset(card_index);
card->capture->start_bm_capture();
}
int free_card_index = -1;
for (unsigned card_index = 0; card_index < num_cards; ++card_index) {
if (cards[card_index].is_fake_capture) {
- free_card_index = int(card_index);
+ free_card_index = card_index;
break;
}
}
fprintf(stderr, "New card plugged in, choosing slot %d.\n", free_card_index);
CaptureCard *card = &cards[free_card_index];
BMUSBCapture *capture = new BMUSBCapture(free_card_index, new_dev);
- configure_card(free_card_index, capture, /*is_fake_capture=*/false);
+ configure_card(free_card_index, capture, /*is_fake_capture=*/false, /*output=*/nullptr);
card->queue_length_policy.reset(free_card_index);
capture->set_card_disconnected_callback(bind(&Mixer::bm_hotplug_remove, this, free_card_index));
capture->start_bm_capture();
resource_pool->release_fbo(fbo);
chroma_subsampler->subsample_chroma(cbcr_full_tex, global_flags.width, global_flags.height, cbcr_tex);
+ if (output_card_index != -1) {
+ cards[output_card_index].output->send_frame(y_tex, cbcr_full_tex, theme_main_chain.input_frames, pts_int, duration);
+ }
resource_pool->release_2d_texture(cbcr_full_tex);
// Set the right state for rgba_tex.
if (alsa) {
alsa->write(samples_out);
}
- decklink_output->send_audio(task.pts_int, samples_out);
+ if (output_card_index != -1) {
+ const int64_t av_delay = lrint(global_flags.audio_queue_length_ms * 0.001 * TIMEBASE); // Corresponds to the delay in ResamplingQueue.
+ cards[output_card_index].output->send_audio(task.pts_int + av_delay, samples_out);
+ }
video_encoder->add_audio(task.pts_int, move(samples_out));
}
}
class ALSAOutput;
class ChromaSubsampler;
+class DeckLinkOutput;
class QSurface;
class QSurfaceFormat;
}
private:
- void configure_card(unsigned card_index, bmusb::CaptureInterface *capture, bool is_fake_capture);
+ void configure_card(unsigned card_index, bmusb::CaptureInterface *capture, bool is_fake_capture, DeckLinkOutput *output);
+ void set_output_card(int card_index); // -1 = no output, just stream.
void bm_frame(unsigned card_index, uint16_t timecode,
bmusb::FrameAllocator::Frame video_frame, size_t video_offset, bmusb::VideoFormat video_format,
bmusb::FrameAllocator::Frame audio_frame, size_t audio_offset, bmusb::AudioFormat audio_format);
HTTPD httpd;
unsigned num_cards;
- QSurface *mixer_surface, *h264_encoder_surface;
+ QSurface *mixer_surface, *h264_encoder_surface, *decklink_output_surface;
std::unique_ptr<movit::ResourcePool> resource_pool;
std::unique_ptr<Theme> theme;
std::atomic<unsigned> audio_source_channel{0};
- std::atomic<unsigned> master_clock_channel{0};
+ std::atomic<int> master_clock_channel{0}; // Gets overridden by <output_card_index> if set.
+ std::atomic<int> output_card_index{-1}; // -1 for none.
std::unique_ptr<movit::EffectChain> display_chain;
std::unique_ptr<ChromaSubsampler> chroma_subsampler;
std::unique_ptr<VideoEncoder> video_encoder;
struct CaptureCard {
bmusb::CaptureInterface *capture = nullptr;
bool is_fake_capture;
+ DeckLinkOutput *output = nullptr;
+
+ // If this card is used for output (ie., output_card_index points to it),
+ // it cannot simultaneously be uesd for capture, so <capture> gets replaced
+ // by a FakeCapture. However, since reconstructing the real capture object
+ // with all its state can be annoying, it is not being deleted, just stopped
+ // and moved here.
+ bmusb::CaptureInterface *parked_capture = nullptr;
+
std::unique_ptr<PBOFrameAllocator> frame_allocator;
// Stuff for the OpenGL context (for texture uploading).
};
CaptureCard cards[MAX_VIDEO_CARDS]; // Protected by <card_mutex>.
AudioMixer audio_mixer; // Same as global_audio_mixer (see audio_mixer.h).
+ bool input_card_is_master_clock(unsigned card_index, unsigned master_card_index) const;
struct OutputFrameInfo {
int dropped_frames; // Since last frame.
int num_samples; // Audio samples needed for this output frame.
int64_t frame_duration; // In TIMEBASE units.
};
- OutputFrameInfo get_one_frame_from_each_card(unsigned master_card_index, CaptureCard::NewFrame new_frames[MAX_VIDEO_CARDS], bool has_new_frame[MAX_VIDEO_CARDS]);
+ OutputFrameInfo 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]);
InputState input_state;
if (is_main_chain) {
YCbCrFormat output_ycbcr_format;
- // We actually output 4:2:0 in the end, but chroma subsampling
+ // We actually output 4:2:0 and/or 4:2:2 in the end, but chroma subsampling
// happens in a pass not run by Movit (see ChromaSubsampler::subsample_chroma()).
output_ycbcr_format.chroma_subsampling_x = 1;
output_ycbcr_format.chroma_subsampling_y = 1;