]> git.sesse.net Git - nageru/commitdiff
Add support for DeckLink HDMI/SDI output.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 18 Jan 2017 22:06:13 +0000 (23:06 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 18 Jan 2017 22:06:13 +0000 (23:06 +0100)
This is pretty raw still; audio isn't tested much, there's no
documentation, hardcoded 720p60 and no GUI control. But most
of the basic ideas are in place, so it should be a reasonable
base to build on.

Makefile
decklink_output.cpp [new file with mode: 0644]
decklink_output.h [new file with mode: 0644]
flags.cpp
flags.h
mixer.cpp
mixer.h
theme.cpp

index 2d2d60950dca9d5768fe36416a597bd66425e3f0..dba6200f7b122f1dbdfb6987e682a180b0afa5c5 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -28,7 +28,7 @@ OBJS += chroma_subsampler.o mixer.o pbo_frame_allocator.o context.o ref_counted_
 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)
diff --git a/decklink_output.cpp b/decklink_output.cpp
new file mode 100644 (file)
index 0000000..46d9b14
--- /dev/null
@@ -0,0 +1,483 @@
+#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;
+}
diff --git a/decklink_output.h b/decklink_output.h
new file mode 100644 (file)
index 0000000..0aba506
--- /dev/null
@@ -0,0 +1,134 @@
+#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)
index 02ea81b6fd8ed4174916a344151f33eb2b238889..709cfb601ce82d70fb15015573ed0b7201ed4659 100644 (file)
--- a/flags.cpp
+++ b/flags.cpp
@@ -47,7 +47,9 @@ enum LongOption {
        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()
@@ -58,6 +60,7 @@ 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");
@@ -102,9 +105,17 @@ void usage()
        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[])
@@ -114,6 +125,7 @@ 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' },
@@ -153,10 +165,12 @@ void parse_flags(int argc, char * const argv[])
                { "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);
@@ -174,6 +188,9 @@ void parse_flags(int argc, char * const argv[])
                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;
@@ -312,6 +329,12 @@ void parse_flags(int argc, char * const argv[])
                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);
@@ -332,6 +355,11 @@ void parse_flags(int argc, char * const argv[])
                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");
@@ -362,12 +390,45 @@ void parse_flags(int argc, char * const argv[])
                }
        }
 
-       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);
        }
 }
diff --git a/flags.h b/flags.h
index 06b08d1f160f88ddabb5b594d9912b82709c03a8..bc869526b88007164699bda284b41fe793c08e0f 100644 (file)
--- a/flags.h
+++ b/flags.h
@@ -43,6 +43,9 @@ struct Flags {
        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;
 
index 7070bd0883d4797c1722bc176308001deb69d247..d55363fa1d367d12aba0cbfd564d327a9c2bfa51 100644 (file)
--- a/mixer.cpp
+++ b/mixer.cpp
@@ -35,6 +35,7 @@
 #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"
@@ -108,6 +109,7 @@ 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)),
          audio_mixer(num_cards)
 {
        CHECK(init_movit(MOVIT_SHADER_DIR, MOVIT_DEBUG_OFF));
@@ -156,7 +158,10 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
                                        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();
@@ -165,18 +170,19 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
                        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) {
@@ -195,6 +201,9 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards)
        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()
@@ -213,7 +222,7 @@ 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);
 
@@ -224,6 +233,7 @@ void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, bool
        }
        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.
@@ -244,6 +254,36 @@ void Mixer::configure_card(unsigned card_index, CaptureInterface *capture, bool
        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 {
 
@@ -523,7 +563,9 @@ void Mixer::thread_func()
        // 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;
@@ -536,10 +578,18 @@ void Mixer::thread_func()
                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;
 
@@ -560,7 +610,7 @@ void Mixer::thread_func()
 
                // 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;
@@ -648,17 +698,35 @@ void Mixer::thread_func()
        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());
@@ -669,7 +737,7 @@ start:
        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;
                }
@@ -678,7 +746,7 @@ start:
                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.
@@ -693,8 +761,10 @@ start:
                }
        }
 
-       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.
@@ -714,7 +784,7 @@ void Mixer::handle_hotplugged_cards()
                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();
                }
@@ -731,7 +801,7 @@ void Mixer::handle_hotplugged_cards()
                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;
                        }
                }
@@ -744,7 +814,7 @@ void Mixer::handle_hotplugged_cards()
                        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();
@@ -808,6 +878,9 @@ void Mixer::render_one_frame(int64_t duration)
        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.
@@ -871,7 +944,10 @@ void Mixer::audio_thread_func()
                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));
        }
 }
diff --git a/mixer.h b/mixer.h
index b5ae946575937dd6cb2bcacd29b8a831064a2173..58084817c70c9c78abef8ff9801ac71d9bd30778 100644 (file)
--- a/mixer.h
+++ b/mixer.h
@@ -38,6 +38,7 @@
 
 class ALSAOutput;
 class ChromaSubsampler;
+class DeckLinkOutput;
 class QSurface;
 class QSurfaceFormat;
 
@@ -285,7 +286,8 @@ public:
        }
 
 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);
@@ -303,11 +305,12 @@ private:
        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;
@@ -326,6 +329,15 @@ private:
        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).
@@ -350,12 +362,13 @@ private:
        };
        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;
 
index 24c6cdc261b93114a6a98a6ef222dfdcb9f8d18f..bcdd6f37bad5d69cd6462f6d0732064c21896a9d 100644 (file)
--- a/theme.cpp
+++ b/theme.cpp
@@ -261,7 +261,7 @@ int EffectChain_finalize(lua_State* L)
 
        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;