aja-output.cpp 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265
  1. #include "aja-card-manager.hpp"
  2. #include "aja-common.hpp"
  3. #include "aja-ui-props.hpp"
  4. #include "aja-output.hpp"
  5. #include "aja-routing.hpp"
  6. #include <obs-module.h>
  7. #include <util/platform.h>
  8. #include <ajabase/common/timer.h>
  9. #include <ajabase/system/systemtime.h>
  10. #include <ajantv2/includes/ntv2card.h>
  11. #include <ajantv2/includes/ntv2devicefeatures.h>
  12. #include <atomic>
  13. #include <stdlib.h>
  14. // Log AJA Output video/audio delay and av-sync
  15. // #define AJA_OUTPUT_STATS
  16. static constexpr uint32_t kNumCardFrames = 3;
  17. static const int64_t kDefaultStatPeriod = 3000000000;
  18. static const int64_t kAudioSyncAdjust = 20000;
  19. static void copy_audio_data(struct audio_data *src, struct audio_data *dst,
  20. size_t size)
  21. {
  22. if (src->data[0]) {
  23. dst->data[0] = (uint8_t *)bmemdup(src->data[0], size);
  24. }
  25. }
  26. static void free_audio_data(struct audio_data *frames)
  27. {
  28. if (frames->data[0]) {
  29. bfree(frames->data[0]);
  30. frames->data[0] = NULL;
  31. }
  32. memset(frames, 0, sizeof(*frames));
  33. }
  34. static void copy_video_data(struct video_data *src, struct video_data *dst,
  35. size_t size)
  36. {
  37. if (src->data[0]) {
  38. dst->data[0] = (uint8_t *)bmemdup(src->data[0], size);
  39. }
  40. }
  41. static void free_video_frame(struct video_data *frame)
  42. {
  43. if (frame->data[0]) {
  44. bfree(frame->data[0]);
  45. frame->data[0] = NULL;
  46. }
  47. memset(frame, 0, sizeof(*frame));
  48. }
  49. AJAOutput::AJAOutput(CNTV2Card *card, const std::string &cardID,
  50. const std::string &outputID, UWord deviceIndex,
  51. const NTV2DeviceID deviceID)
  52. : mCardID{cardID},
  53. mOutputID{outputID},
  54. mDeviceIndex{deviceIndex},
  55. mDeviceID{deviceID},
  56. mFrameTimes{},
  57. mAudioPlayCursor{0},
  58. mAudioWriteCursor{0},
  59. mAudioWrapAddress{0},
  60. mAudioRate{0},
  61. mAudioQueueSamples{0},
  62. mAudioWriteSamples{0},
  63. mAudioPlaySamples{0},
  64. mNumCardFrames{0},
  65. mFirstCardFrame{0},
  66. mLastCardFrame{0},
  67. mWriteCardFrame{0},
  68. mPlayCardFrame{0},
  69. mPlayCardNext{0},
  70. mFrameRateNum{0},
  71. mFrameRateDen{0},
  72. mVideoQueueFrames{0},
  73. mVideoWriteFrames{0},
  74. mVideoPlayFrames{0},
  75. mFirstVideoTS{0},
  76. mFirstAudioTS{0},
  77. mLastVideoTS{0},
  78. mLastAudioTS{0},
  79. mVideoDelay{0},
  80. mAudioDelay{0},
  81. mAudioVideoSync{0},
  82. mAudioAdjust{0},
  83. mLastStatTime{0},
  84. #ifdef AJA_WRITE_DEBUG_WAV
  85. mWaveWriter{nullptr},
  86. #endif
  87. mCard{card},
  88. mOutputProps{DEVICE_ID_NOTFOUND},
  89. mTestPattern{},
  90. mIsRunning{false},
  91. mAudioStarted{false},
  92. mRunThread{},
  93. mVideoLock{},
  94. mAudioLock{},
  95. mRunThreadLock{},
  96. mVideoQueue{},
  97. mAudioQueue{},
  98. mOBSOutput{nullptr}
  99. {
  100. mVideoQueue = std::make_unique<VideoQueue>();
  101. mAudioQueue = std::make_unique<AudioQueue>();
  102. }
  103. AJAOutput::~AJAOutput()
  104. {
  105. if (mVideoQueue)
  106. mVideoQueue.reset();
  107. if (mAudioQueue)
  108. mAudioQueue.reset();
  109. }
  110. CNTV2Card *AJAOutput::GetCard()
  111. {
  112. return mCard;
  113. }
  114. void AJAOutput::Initialize(const OutputProps &props)
  115. {
  116. const auto &audioSystem = props.AudioSystem();
  117. // Store the address to the end of the card's audio buffer.
  118. mCard->GetAudioWrapAddress(mAudioWrapAddress, audioSystem);
  119. // Specify the frame indices for the "on-air" frames on the card.
  120. // Starts at frame index corresponding to the output Channel * numFrames
  121. calculate_card_frame_indices(kNumCardFrames, mCard->GetDeviceID(),
  122. props.Channel(), props.videoFormat,
  123. props.pixelFormat);
  124. mCard->SetOutputFrame(props.Channel(), mWriteCardFrame);
  125. mCard->WaitForOutputVerticalInterrupt(props.Channel());
  126. const auto &cardFrameRate =
  127. GetNTV2FrameRateFromVideoFormat(props.videoFormat);
  128. ULWord fpsNum = 0;
  129. ULWord fpsDen = 0;
  130. GetFramesPerSecond(cardFrameRate, fpsNum, fpsDen);
  131. mFrameRateNum = fpsNum;
  132. mFrameRateDen = fpsDen;
  133. mFrameTimes.cardFrameTime =
  134. (1000000000ULL / (uint64_t)(fpsNum / fpsDen));
  135. mFrameTimes.cardFps = (double)(fpsNum / fpsDen);
  136. mFrameTimes.obsFps = obs_get_active_fps();
  137. if (mFrameTimes.obsFps < 1.0)
  138. mFrameTimes.obsFps = 30.0;
  139. mFrameTimes.obsFrameTime =
  140. (1000000000ULL / (uint64_t)mFrameTimes.obsFps);
  141. mVideoDelay = ((int64_t)mNumCardFrames - 0) * 1000000 * mFrameRateDen /
  142. mFrameRateNum;
  143. mAudioRate = props.audioSampleRate;
  144. SetOutputProps(props);
  145. }
  146. void AJAOutput::SetOBSOutput(obs_output_t *output)
  147. {
  148. mOBSOutput = output;
  149. }
  150. obs_output_t *AJAOutput::GetOBSOutput()
  151. {
  152. return mOBSOutput;
  153. }
  154. void AJAOutput::SetOutputProps(const OutputProps &props)
  155. {
  156. mOutputProps = props;
  157. }
  158. OutputProps AJAOutput::GetOutputProps() const
  159. {
  160. return mOutputProps;
  161. }
  162. void AJAOutput::GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
  163. NTV2TestPatternSelect pattern)
  164. {
  165. NTV2VideoFormat vid_fmt = vf;
  166. NTV2PixelFormat pix_fmt = pf;
  167. if (vid_fmt == NTV2_FORMAT_UNKNOWN)
  168. vid_fmt = NTV2_FORMAT_720p_5994;
  169. if (pix_fmt == NTV2_FBF_INVALID)
  170. pix_fmt = kDefaultAJAPixelFormat;
  171. NTV2FormatDesc fd(vid_fmt, pix_fmt, NTV2_VANCMODE_OFF);
  172. auto bufSize = fd.GetTotalRasterBytes();
  173. // Raster size changed, regenerate pattern
  174. if (bufSize != mTestPattern.size()) {
  175. mTestPattern.clear();
  176. mTestPattern.resize(bufSize);
  177. NTV2TestPatternGen gen;
  178. gen.DrawTestPattern(pattern, fd.GetRasterWidth(),
  179. fd.GetRasterHeight(), pix_fmt,
  180. mTestPattern);
  181. }
  182. if (mTestPattern.size() == 0) {
  183. blog(LOG_DEBUG,
  184. "AJAOutput::GenerateTestPattern: Error generating test pattern!");
  185. return;
  186. }
  187. auto outputChannel = mOutputProps.Channel();
  188. mCard->SetOutputFrame(outputChannel, mWriteCardFrame);
  189. mCard->DMAWriteFrame(
  190. mWriteCardFrame,
  191. reinterpret_cast<ULWord *>(&mTestPattern.data()[0]),
  192. static_cast<ULWord>(mTestPattern.size()));
  193. }
  194. void AJAOutput::QueueVideoFrame(struct video_data *frame, size_t size)
  195. {
  196. const std::lock_guard<std::mutex> lock(mVideoLock);
  197. VideoFrame vf;
  198. vf.frame = *frame;
  199. vf.frameNum = mVideoWriteFrames;
  200. vf.size = size;
  201. if (mVideoQueue->size() > kVideoQueueMaxSize) {
  202. auto &front = mVideoQueue->front();
  203. free_video_frame(&front.frame);
  204. mVideoQueue->pop_front();
  205. }
  206. copy_video_data(frame, &vf.frame, size);
  207. mVideoQueue->push_back(vf);
  208. mVideoQueueFrames++;
  209. }
  210. void AJAOutput::QueueAudioFrames(struct audio_data *frames, size_t size)
  211. {
  212. const std::lock_guard<std::mutex> lock(mAudioLock);
  213. AudioFrames af;
  214. af.frames = *frames;
  215. af.offset = 0;
  216. af.size = size;
  217. if (mAudioQueue->size() > kAudioQueueMaxSize) {
  218. auto &front = mAudioQueue->front();
  219. free_audio_data(&front.frames);
  220. mAudioQueue->pop_front();
  221. }
  222. copy_audio_data(frames, &af.frames, size);
  223. mAudioQueue->push_back(af);
  224. mAudioQueueSamples +=
  225. size / (kDefaultAudioChannels * kDefaultAudioSampleSize);
  226. }
  227. void AJAOutput::ClearVideoQueue()
  228. {
  229. const std::lock_guard<std::mutex> lock(mVideoLock);
  230. while (mVideoQueue->size() > 0) {
  231. auto &vf = mVideoQueue->front();
  232. free_video_frame(&vf.frame);
  233. mVideoQueue->pop_front();
  234. }
  235. }
  236. void AJAOutput::ClearAudioQueue()
  237. {
  238. const std::lock_guard<std::mutex> lock(mAudioLock);
  239. while (mAudioQueue->size() > 0) {
  240. auto &af = mAudioQueue->front();
  241. free_audio_data(&af.frames);
  242. mAudioQueue->pop_front();
  243. }
  244. }
  245. bool AJAOutput::HaveEnoughAudio(size_t needAudioSize)
  246. {
  247. bool ok = false;
  248. if (mAudioQueue->size() > 0) {
  249. size_t available = 0;
  250. for (size_t i = 0; i < mAudioQueue->size(); i++) {
  251. AudioFrames af = mAudioQueue->at(i);
  252. available += af.size - af.offset;
  253. if (available >= needAudioSize) {
  254. ok = true;
  255. break;
  256. }
  257. }
  258. }
  259. return ok;
  260. }
  261. size_t AJAOutput::VideoQueueSize()
  262. {
  263. return mVideoQueue->size();
  264. }
  265. size_t AJAOutput::AudioQueueSize()
  266. {
  267. return mAudioQueue->size();
  268. }
  269. // lock audio queue before calling
  270. void AJAOutput::DMAAudioFromQueue(NTV2AudioSystem audioSys)
  271. {
  272. AudioFrames &af = mAudioQueue->front();
  273. size_t sizeLeft = af.size - af.offset;
  274. if (!mFirstAudioTS)
  275. mFirstAudioTS = af.frames.timestamp;
  276. mLastAudioTS = af.frames.timestamp;
  277. if (sizeLeft == 0) {
  278. free_audio_data(&af.frames);
  279. mAudioQueue->pop_front();
  280. return;
  281. }
  282. // Get audio play cursor
  283. mCard->ReadAudioLastOut(mAudioPlayCursor, audioSys);
  284. // Calculate audio delay
  285. uint32_t audioPlaySamples = 0;
  286. if (mAudioPlayCursor <= mAudioWriteCursor) {
  287. audioPlaySamples =
  288. (mAudioWriteCursor - mAudioPlayCursor) /
  289. (kDefaultAudioChannels * kDefaultAudioSampleSize);
  290. } else {
  291. audioPlaySamples =
  292. (mAudioWrapAddress - mAudioPlayCursor +
  293. mAudioWriteCursor) /
  294. (kDefaultAudioChannels * kDefaultAudioSampleSize);
  295. }
  296. mAudioDelay = 1000000 * (int64_t)audioPlaySamples / mAudioRate;
  297. // Adjust audio sync when requested
  298. if (mAudioAdjust != 0) {
  299. if (mAudioAdjust > 0) {
  300. // Throw away some samples to resync audio
  301. uint32_t adjustSamples =
  302. (uint32_t)mAudioAdjust * mAudioRate / 1000000;
  303. uint32_t adjustSize = adjustSamples *
  304. kDefaultAudioSampleSize *
  305. kDefaultAudioChannels;
  306. if (adjustSize <= sizeLeft) {
  307. af.offset += adjustSize;
  308. sizeLeft -= adjustSize;
  309. mAudioAdjust = 0;
  310. blog(LOG_DEBUG,
  311. "AJAOutput::DMAAudioFromQueue: Drop %d audio samples",
  312. adjustSamples);
  313. } else {
  314. uint32_t samples = (uint32_t)sizeLeft /
  315. (kDefaultAudioSampleSize *
  316. kDefaultAudioChannels);
  317. af.offset += sizeLeft;
  318. sizeLeft = 0;
  319. adjustSamples -= samples;
  320. mAudioAdjust =
  321. adjustSamples * 1000000 / mAudioRate;
  322. blog(LOG_DEBUG,
  323. "AJAOutput::DMAAudioFromQueue: Drop %d audio samples",
  324. samples);
  325. }
  326. } else {
  327. // Add some silence to resync audio
  328. uint32_t adjustSamples = (uint32_t)(-mAudioAdjust) *
  329. mAudioRate / 1000000;
  330. uint32_t adjustSize = adjustSamples *
  331. kDefaultAudioSampleSize *
  332. kDefaultAudioChannels;
  333. uint8_t *silentBuffer = new uint8_t[adjustSize];
  334. memset(silentBuffer, 0, adjustSize);
  335. dma_audio_samples(audioSys, (uint32_t *)silentBuffer,
  336. adjustSize);
  337. delete[] silentBuffer;
  338. mAudioAdjust = 0;
  339. blog(LOG_DEBUG,
  340. "AJAOutput::DMAAudioFromQueue: Add %d audio samples",
  341. adjustSamples);
  342. }
  343. }
  344. // Write audio to the hardware ring
  345. if (af.frames.data[0] && sizeLeft > 0) {
  346. dma_audio_samples(audioSys,
  347. (uint32_t *)&af.frames.data[0][af.offset],
  348. sizeLeft);
  349. af.offset += sizeLeft;
  350. }
  351. // Free the audio buffer
  352. if (af.offset == af.size) {
  353. free_audio_data(&af.frames);
  354. mAudioQueue->pop_front();
  355. }
  356. }
  357. // lock video queue before calling
  358. void AJAOutput::DMAVideoFromQueue()
  359. {
  360. auto &vf = mVideoQueue->front();
  361. auto data = vf.frame.data[0];
  362. if (!mFirstVideoTS)
  363. mFirstVideoTS = vf.frame.timestamp;
  364. mLastVideoTS = vf.frame.timestamp;
  365. // find the next buffer
  366. uint32_t writeCardFrame = mWriteCardFrame + 1;
  367. if (writeCardFrame > mLastCardFrame)
  368. writeCardFrame = mFirstCardFrame;
  369. // use the next buffer if available
  370. if (writeCardFrame != mPlayCardFrame)
  371. mWriteCardFrame = writeCardFrame;
  372. mVideoWriteFrames++;
  373. auto result = mCard->DMAWriteFrame(mWriteCardFrame,
  374. reinterpret_cast<ULWord *>(data),
  375. (ULWord)vf.size);
  376. if (!result)
  377. blog(LOG_DEBUG,
  378. "AJAOutput::DMAVideoFromQueue: Failed ot write video frame!");
  379. free_video_frame(&vf.frame);
  380. mVideoQueue->pop_front();
  381. }
  382. // TODO(paulh): Keep track of framebuffer indices used on the card, between the capture
  383. // and output plugins, so that we can optimize frame index placement in memory and
  384. // reduce unused gaps in between channel frame indices.
  385. void AJAOutput::calculate_card_frame_indices(uint32_t numFrames,
  386. NTV2DeviceID id,
  387. NTV2Channel channel,
  388. NTV2VideoFormat vf,
  389. NTV2PixelFormat pf)
  390. {
  391. ULWord channelIndex = GetIndexForNTV2Channel(channel);
  392. ULWord totalCardFrames = NTV2DeviceGetNumberFrameBuffers(
  393. id, GetNTV2FrameGeometryFromVideoFormat(vf), pf);
  394. mFirstCardFrame = channelIndex * numFrames;
  395. if (mFirstCardFrame < totalCardFrames &&
  396. (mFirstCardFrame + numFrames) < totalCardFrames) {
  397. // Reserve N framebuffers in card DRAM.
  398. mNumCardFrames = numFrames;
  399. mWriteCardFrame = mFirstCardFrame;
  400. mLastCardFrame = mWriteCardFrame + numFrames;
  401. } else {
  402. // otherwise just grab 2 frames to ping-pong between
  403. mNumCardFrames = 2;
  404. mWriteCardFrame = channelIndex * 2;
  405. mLastCardFrame = mWriteCardFrame + 2;
  406. }
  407. }
  408. uint32_t AJAOutput::get_frame_count()
  409. {
  410. uint32_t frameCount = 0;
  411. NTV2Channel channel = mOutputProps.Channel();
  412. INTERRUPT_ENUMS interrupt = NTV2ChannelToOutputInterrupt(channel);
  413. bool isProgressiveTransport = NTV2_IS_PROGRESSIVE_STANDARD(
  414. ::GetNTV2StandardFromVideoFormat(mOutputProps.videoFormat));
  415. if (isProgressiveTransport) {
  416. mCard->GetInterruptCount(interrupt, frameCount);
  417. } else {
  418. uint32_t intCount;
  419. uint32_t nextCount;
  420. NTV2FieldID fieldID;
  421. mCard->GetInterruptCount(interrupt, intCount);
  422. mCard->GetOutputFieldID(channel, fieldID);
  423. mCard->GetInterruptCount(interrupt, nextCount);
  424. if (intCount != nextCount) {
  425. mCard->GetInterruptCount(interrupt, intCount);
  426. mCard->GetOutputFieldID(channel, fieldID);
  427. }
  428. if (fieldID == NTV2_FIELD1)
  429. intCount--;
  430. frameCount = intCount / 2;
  431. }
  432. return frameCount;
  433. }
  434. // Perform DMA of audio samples to AJA card while taking into account wrapping around the
  435. // ends of the card's audio buffer (size set to 4MB in Routing::ConfigureOutputAudio).
  436. void AJAOutput::dma_audio_samples(NTV2AudioSystem audioSys, uint32_t *data,
  437. size_t size)
  438. {
  439. bool result = false;
  440. mAudioWriteSamples +=
  441. size / (kDefaultAudioChannels * kDefaultAudioSampleSize);
  442. if ((mAudioWriteCursor + size) > mAudioWrapAddress) {
  443. const uint32_t remainingBuffer =
  444. mAudioWrapAddress - mAudioWriteCursor;
  445. auto audioDataRemain = reinterpret_cast<const ULWord *>(
  446. (uint8_t *)(data) + remainingBuffer);
  447. // Incoming audio size will wrap around the end of the card audio buffer.
  448. // Transfer enough bytes to fill to the end of the buffer...
  449. if (remainingBuffer > 0) {
  450. result = mCard->DMAWriteAudio(audioSys, data,
  451. mAudioWriteCursor,
  452. remainingBuffer);
  453. if (!result) {
  454. blog(LOG_DEBUG,
  455. "AJAOutput::dma_audio_samples: "
  456. "failed to write bytes at end of buffer (address = %d)",
  457. mAudioWriteCursor);
  458. }
  459. }
  460. // ...transfer remaining bytes at the front of the card audio buffer.
  461. if (size - remainingBuffer > 0) {
  462. result = mCard->DMAWriteAudio(
  463. audioSys, audioDataRemain, 0,
  464. (uint32_t)size - remainingBuffer);
  465. if (!result) {
  466. blog(LOG_DEBUG,
  467. "AJAOutput::dma_audio_samples "
  468. "failed to write bytes at front of buffer (address = %d)",
  469. mAudioWriteCursor);
  470. }
  471. }
  472. mAudioWriteCursor = (uint32_t)size - remainingBuffer;
  473. } else {
  474. // No wrap, so just do a linear DMA from the buffer...
  475. if (size > 0) {
  476. result = mCard->DMAWriteAudio(audioSys, data,
  477. mAudioWriteCursor,
  478. (ULWord)size);
  479. if (!result) {
  480. blog(LOG_DEBUG,
  481. "AJAOutput::dma_audio_samples "
  482. "failed to write bytes to buffer (address = %d)",
  483. mAudioWriteCursor);
  484. }
  485. }
  486. mAudioWriteCursor += (uint32_t)size;
  487. }
  488. }
  489. void AJAOutput::CreateThread(bool enable)
  490. {
  491. const std::lock_guard<std::mutex> lock(mRunThreadLock);
  492. if (!mRunThread.Active()) {
  493. mRunThread.SetPriority(AJA_ThreadPriority_High);
  494. mRunThread.SetThreadName("AJA Video Output Thread");
  495. mRunThread.Attach(AJAOutput::OutputThread, this);
  496. }
  497. if (enable) {
  498. mIsRunning = true;
  499. mRunThread.Start();
  500. }
  501. }
  502. void AJAOutput::StopThread()
  503. {
  504. const std::lock_guard<std::mutex> lock(mRunThreadLock);
  505. mIsRunning = false;
  506. if (mRunThread.Active()) {
  507. mRunThread.Stop();
  508. }
  509. }
  510. bool AJAOutput::ThreadRunning()
  511. {
  512. return mIsRunning;
  513. }
  514. void AJAOutput::OutputThread(AJAThread *thread, void *ctx)
  515. {
  516. UNUSED_PARAMETER(thread);
  517. AJAOutput *ajaOutput = static_cast<AJAOutput *>(ctx);
  518. if (!ajaOutput) {
  519. blog(LOG_ERROR,
  520. "AJAOutput::OutputThread: AJA Output instance is null!");
  521. return;
  522. }
  523. CNTV2Card *card = ajaOutput->GetCard();
  524. if (!card) {
  525. blog(LOG_ERROR,
  526. "AJAOutput::OutputThread: Card instance is null!");
  527. return;
  528. }
  529. const auto &props = ajaOutput->GetOutputProps();
  530. const auto &audioSystem = props.AudioSystem();
  531. uint64_t videoPlayLast = ajaOutput->get_frame_count();
  532. uint32_t audioSyncCount = 0;
  533. uint32_t videoSyncCount = 0;
  534. uint32_t syncCountMax = 5;
  535. int64_t audioSyncSum = 0;
  536. int64_t videoSyncSum = 0;
  537. // thread loop
  538. while (ajaOutput->ThreadRunning()) {
  539. // Wait for preroll
  540. if (!ajaOutput->mAudioStarted &&
  541. (ajaOutput->mAudioDelay > ajaOutput->mVideoDelay)) {
  542. card->StartAudioOutput(audioSystem, false);
  543. ajaOutput->mAudioStarted = true;
  544. blog(LOG_DEBUG,
  545. "AJAOutput::OutputThread: Audio Preroll complete");
  546. }
  547. // Check if a vsync occurred
  548. uint32_t frameCount = ajaOutput->get_frame_count();
  549. if (frameCount > videoPlayLast) {
  550. videoPlayLast = frameCount;
  551. ajaOutput->mPlayCardFrame = ajaOutput->mPlayCardNext;
  552. if (ajaOutput->mPlayCardFrame !=
  553. ajaOutput->mWriteCardFrame) {
  554. uint32_t playCardNext =
  555. ajaOutput->mPlayCardFrame + 1;
  556. if (playCardNext > ajaOutput->mLastCardFrame)
  557. playCardNext =
  558. ajaOutput->mFirstCardFrame;
  559. if (playCardNext !=
  560. ajaOutput->mWriteCardFrame) {
  561. ajaOutput->mPlayCardNext = playCardNext;
  562. // Increment the play frame
  563. ajaOutput->mCard->SetOutputFrame(
  564. ajaOutput->mOutputProps
  565. .Channel(),
  566. ajaOutput->mPlayCardNext);
  567. }
  568. ajaOutput->mVideoPlayFrames++;
  569. }
  570. }
  571. // Audio DMA
  572. {
  573. const std::lock_guard<std::mutex> lock(
  574. ajaOutput->mAudioLock);
  575. while (ajaOutput->AudioQueueSize() > 0) {
  576. ajaOutput->DMAAudioFromQueue(audioSystem);
  577. }
  578. }
  579. // Video DMA
  580. {
  581. const std::lock_guard<std::mutex> lock(
  582. ajaOutput->mVideoLock);
  583. while (ajaOutput->VideoQueueSize() > 0) {
  584. ajaOutput->DMAVideoFromQueue();
  585. }
  586. }
  587. // Get current time and audio play cursor
  588. int64_t curTime = (int64_t)os_gettime_ns();
  589. card->ReadAudioLastOut(ajaOutput->mAudioPlayCursor,
  590. audioSystem);
  591. if (ajaOutput->mAudioStarted &&
  592. ((curTime - ajaOutput->mLastStatTime) >
  593. kDefaultStatPeriod)) {
  594. ajaOutput->mLastStatTime = curTime;
  595. // Calculate av sync delay
  596. ajaOutput->mAudioVideoSync =
  597. ajaOutput->mAudioDelay - ajaOutput->mVideoDelay;
  598. if (ajaOutput->mAudioVideoSync > kAudioSyncAdjust) {
  599. audioSyncCount++;
  600. audioSyncSum += ajaOutput->mAudioVideoSync;
  601. if (audioSyncCount >= syncCountMax) {
  602. ajaOutput->mAudioAdjust =
  603. audioSyncSum / syncCountMax;
  604. audioSyncCount = 0;
  605. audioSyncSum = 0;
  606. }
  607. } else {
  608. audioSyncCount = 0;
  609. audioSyncSum = 0;
  610. }
  611. if (ajaOutput->mAudioVideoSync < -kAudioSyncAdjust) {
  612. videoSyncCount++;
  613. videoSyncSum += ajaOutput->mAudioVideoSync;
  614. if (videoSyncCount >= syncCountMax) {
  615. ajaOutput->mAudioAdjust =
  616. videoSyncSum / syncCountMax;
  617. videoSyncCount = 0;
  618. videoSyncSum = 0;
  619. }
  620. } else {
  621. videoSyncCount = 0;
  622. videoSyncSum = 0;
  623. }
  624. #ifdef AJA_OUTPUT_STATS
  625. blog(LOG_DEBUG,
  626. "AJAOutput::OutputThread: vd %li ad %li avs %li",
  627. ajaOutput->mVideoDelay, ajaOutput->mAudioDelay,
  628. ajaOutput->mAudioVideoSync);
  629. #endif
  630. }
  631. os_sleep_ms(1);
  632. }
  633. ajaOutput->mAudioStarted = false;
  634. blog(LOG_INFO,
  635. "AJAOutput::OutputThread: Thread stopped. Played %lld video frames",
  636. ajaOutput->mVideoQueueFrames);
  637. }
  638. void populate_output_device_list(obs_property_t *list)
  639. {
  640. obs_property_list_clear(list);
  641. auto &cardManager = aja::CardManager::Instance();
  642. cardManager.EnumerateCards();
  643. for (auto &iter : cardManager.GetCardEntries()) {
  644. if (!iter.second)
  645. continue;
  646. CNTV2Card *card = iter.second->GetCard();
  647. if (!card)
  648. continue;
  649. NTV2DeviceID deviceID = card->GetDeviceID();
  650. //TODO(paulh): Add support for analog I/O
  651. // w/ NTV2DeviceGetNumAnalogVideoOutputs(cardEntry.deviceID)
  652. if (NTV2DeviceGetNumVideoOutputs(deviceID) > 0 ||
  653. NTV2DeviceGetNumHDMIVideoOutputs(deviceID) > 0) {
  654. obs_property_list_add_string(
  655. list, iter.second->GetDisplayName().c_str(),
  656. iter.second->GetCardID().c_str());
  657. }
  658. }
  659. }
  660. bool aja_output_device_changed(void *data, obs_properties_t *props,
  661. obs_property_t *list, obs_data_t *settings)
  662. {
  663. UNUSED_PARAMETER(data);
  664. blog(LOG_DEBUG, "AJA Output Device Changed");
  665. populate_output_device_list(list);
  666. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  667. if (!cardID) {
  668. blog(LOG_ERROR, "aja_output_device_changed: Card ID is null!");
  669. return false;
  670. }
  671. const char *outputID =
  672. obs_data_get_string(settings, kUIPropAJAOutputID.id);
  673. auto &cardManager = aja::CardManager::Instance();
  674. cardManager.EnumerateCards();
  675. auto cardEntry = cardManager.GetCardEntry(cardID);
  676. if (!cardEntry) {
  677. blog(LOG_ERROR,
  678. "aja_output_device_changed: Card Entry not found for %s",
  679. cardID);
  680. return false;
  681. }
  682. CNTV2Card *card = cardEntry->GetCard();
  683. if (!card) {
  684. blog(LOG_ERROR,
  685. "aja_output_device_changed: Card instance is null!");
  686. return false;
  687. }
  688. obs_property_t *io_select_list =
  689. obs_properties_get(props, kUIPropOutput.id);
  690. obs_property_t *vid_fmt_list =
  691. obs_properties_get(props, kUIPropVideoFormatSelect.id);
  692. obs_property_t *pix_fmt_list =
  693. obs_properties_get(props, kUIPropPixelFormatSelect.id);
  694. obs_property_t *sdi_4k_list =
  695. obs_properties_get(props, kUIPropSDI4KTransport.id);
  696. const NTV2DeviceID deviceID = cardEntry->GetDeviceID();
  697. populate_io_selection_output_list(cardID, outputID, deviceID,
  698. io_select_list);
  699. // If Channel 1 is actively in use, filter the video format list to only
  700. // show video formats within the same framerate family. If Channel 1 is
  701. // not active we just go ahead and try to set all framestores to the same video format.
  702. // This is because Channel 1's clock rate will govern the card's Free Run clock.
  703. NTV2VideoFormat videoFormatChannel1 = NTV2_FORMAT_UNKNOWN;
  704. if (!cardEntry->ChannelReady(NTV2_CHANNEL1, outputID)) {
  705. card->GetVideoFormat(videoFormatChannel1, NTV2_CHANNEL1);
  706. }
  707. obs_property_list_clear(vid_fmt_list);
  708. populate_video_format_list(deviceID, vid_fmt_list, videoFormatChannel1);
  709. obs_property_list_clear(pix_fmt_list);
  710. populate_pixel_format_list(deviceID, pix_fmt_list);
  711. obs_property_list_clear(sdi_4k_list);
  712. populate_sdi_4k_transport_list(sdi_4k_list);
  713. return true;
  714. }
  715. bool aja_output_dest_changed(obs_properties_t *props, obs_property_t *list,
  716. obs_data_t *settings)
  717. {
  718. UNUSED_PARAMETER(props);
  719. blog(LOG_DEBUG, "AJA Output Dest Changed");
  720. auto &cardManager = aja::CardManager::Instance();
  721. cardManager.EnumerateCards();
  722. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  723. if (!cardID) {
  724. blog(LOG_ERROR, "aja_output_dest_changed: Card ID is null!");
  725. return false;
  726. }
  727. auto cardEntry = cardManager.GetCardEntry(cardID);
  728. if (!cardEntry) {
  729. blog(LOG_ERROR,
  730. "aja_output_dest_changed: Card entry not found for %s",
  731. cardID);
  732. return false;
  733. }
  734. // Revert to "Select..." if desired IOSelection is already in use
  735. auto io_select = static_cast<IOSelection>(
  736. obs_data_get_int(settings, kUIPropOutput.id));
  737. for (size_t i = 0; i < obs_property_list_item_count(list); i++) {
  738. auto io_item = static_cast<IOSelection>(
  739. obs_property_list_item_int(list, i));
  740. if (io_item == io_select &&
  741. obs_property_list_item_disabled(list, i)) {
  742. obs_data_set_int(
  743. settings, kUIPropOutput.id,
  744. static_cast<long long>(IOSelection::Invalid));
  745. blog(LOG_WARNING,
  746. "aja_output_dest_changed: IOSelection %s is already in use",
  747. aja::IOSelectionToString(io_select).c_str());
  748. return false;
  749. }
  750. }
  751. return true;
  752. }
  753. static void aja_output_destroy(void *data)
  754. {
  755. blog(LOG_DEBUG, "AJA Output Destroy");
  756. auto ajaOutput = (AJAOutput *)data;
  757. if (!ajaOutput) {
  758. blog(LOG_ERROR, "aja_output_destroy: Plugin instance is null!");
  759. return;
  760. }
  761. #ifdef AJA_WRITE_DEBUG_WAV
  762. if (ajaOutput->mWaveWriter) {
  763. ajaOutput->mWaveWriter->close();
  764. delete ajaOutput->mWaveWriter;
  765. ajaOutput->mWaveWriter = nullptr;
  766. }
  767. #endif
  768. ajaOutput->StopThread();
  769. ajaOutput->ClearVideoQueue();
  770. ajaOutput->ClearAudioQueue();
  771. delete ajaOutput;
  772. ajaOutput = nullptr;
  773. }
  774. static void *aja_output_create(obs_data_t *settings, obs_output_t *output)
  775. {
  776. blog(LOG_INFO, "Creating AJA Output...");
  777. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  778. if (!cardID) {
  779. blog(LOG_ERROR, "aja_output_create: Card ID is null!");
  780. return nullptr;
  781. }
  782. const char *outputID =
  783. obs_data_get_string(settings, kUIPropAJAOutputID.id);
  784. auto &cardManager = aja::CardManager::Instance();
  785. auto cardEntry = cardManager.GetCardEntry(cardID);
  786. if (!cardEntry) {
  787. blog(LOG_ERROR,
  788. "aja_output_create: Card Entry not found for %s", cardID);
  789. return nullptr;
  790. }
  791. CNTV2Card *card = cardEntry->GetCard();
  792. if (!card) {
  793. blog(LOG_ERROR,
  794. "aja_output_create: Card instance is null for %s", cardID);
  795. return nullptr;
  796. }
  797. NTV2DeviceID deviceID = card->GetDeviceID();
  798. OutputProps outputProps(deviceID);
  799. outputProps.ioSelect = static_cast<IOSelection>(
  800. obs_data_get_int(settings, kUIPropOutput.id));
  801. if (outputProps.ioSelect == IOSelection::Invalid) {
  802. blog(LOG_DEBUG,
  803. "aja_output_create: Select a valid AJA Output IOSelection!");
  804. return nullptr;
  805. }
  806. outputProps.videoFormat = static_cast<NTV2VideoFormat>(
  807. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  808. outputProps.pixelFormat = static_cast<NTV2PixelFormat>(
  809. obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
  810. outputProps.sdi4kTransport = static_cast<SDI4KTransport>(
  811. obs_data_get_int(settings, kUIPropSDI4KTransport.id));
  812. outputProps.audioNumChannels = kDefaultAudioChannels;
  813. outputProps.audioSampleSize = kDefaultAudioSampleSize;
  814. outputProps.audioSampleRate = kDefaultAudioSampleRate;
  815. if (NTV2_IS_4K_VIDEO_FORMAT(outputProps.videoFormat) &&
  816. outputProps.sdi4kTransport == SDI4KTransport::Squares) {
  817. if (outputProps.ioSelect == IOSelection::SDI1_2) {
  818. outputProps.ioSelect = IOSelection::SDI1_2_Squares;
  819. } else if (outputProps.ioSelect == IOSelection::SDI3_4) {
  820. outputProps.ioSelect = IOSelection::SDI3_4_Squares;
  821. }
  822. }
  823. const std::string &ioSelectStr =
  824. aja::IOSelectionToString(outputProps.ioSelect);
  825. NTV2OutputDestinations outputDests;
  826. aja::IOSelectionToOutputDests(outputProps.ioSelect, outputDests);
  827. if (outputDests.empty()) {
  828. blog(LOG_ERROR,
  829. "No Output Destinations found for IOSelection %s!",
  830. ioSelectStr.c_str());
  831. return nullptr;
  832. }
  833. outputProps.outputDest = *outputDests.begin();
  834. if (!cardEntry->AcquireOutputSelection(outputProps.ioSelect, deviceID,
  835. outputID)) {
  836. blog(LOG_ERROR,
  837. "aja_output_create: Error acquiring IOSelection %s for card ID %s",
  838. ioSelectStr.c_str(), cardID);
  839. return nullptr;
  840. }
  841. auto ajaOutput = new AJAOutput(card, cardID, outputID,
  842. (UWord)cardEntry->GetCardIndex(),
  843. deviceID);
  844. ajaOutput->Initialize(outputProps);
  845. ajaOutput->ClearVideoQueue();
  846. ajaOutput->ClearAudioQueue();
  847. ajaOutput->SetOBSOutput(output);
  848. ajaOutput->CreateThread(true);
  849. #ifdef AJA_WRITE_DEBUG_WAV
  850. AJAWavWriterAudioFormat wavFormat;
  851. wavFormat.channelCount = outputProps.AudioChannels();
  852. wavFormat.sampleRate = outputProps.audioSampleRate;
  853. wavFormat.sampleSize = outputProps.AudioSize();
  854. ajaOutput->mWaveWriter =
  855. new AJAWavWriter("obs_aja_output.wav", wavFormat);
  856. ajaOutput->mWaveWriter->open();
  857. #endif
  858. blog(LOG_INFO, "AJA Output created!");
  859. return ajaOutput;
  860. }
  861. static void aja_output_update(void *data, obs_data_t *settings)
  862. {
  863. UNUSED_PARAMETER(data);
  864. UNUSED_PARAMETER(settings);
  865. blog(LOG_INFO, "AJA Output Update...");
  866. }
  867. static bool aja_output_start(void *data)
  868. {
  869. blog(LOG_INFO, "Starting AJA Output...");
  870. auto ajaOutput = (AJAOutput *)data;
  871. if (!ajaOutput) {
  872. blog(LOG_ERROR, "aja_output_start: Plugin instance is null!");
  873. return false;
  874. }
  875. const std::string &cardID = ajaOutput->mCardID;
  876. auto &cardManager = aja::CardManager::Instance();
  877. cardManager.EnumerateCards();
  878. auto cardEntry = cardManager.GetCardEntry(cardID);
  879. if (!cardEntry) {
  880. blog(LOG_DEBUG,
  881. "aja_io_selection_changed: Card Entry not found for %s",
  882. cardID.c_str());
  883. return false;
  884. }
  885. CNTV2Card *card = ajaOutput->GetCard();
  886. if (!card) {
  887. blog(LOG_ERROR, "aja_output_start: Card instance is null!");
  888. return false;
  889. }
  890. auto outputProps = ajaOutput->GetOutputProps();
  891. auto audioSystem = outputProps.AudioSystem();
  892. auto outputDest = outputProps.outputDest;
  893. auto videoFormat = outputProps.videoFormat;
  894. auto pixelFormat = outputProps.pixelFormat;
  895. blog(LOG_INFO,
  896. "Output Dest: %s | Audio System: %s | Video Format: %s | Pixel Format: %s",
  897. NTV2OutputDestinationToString(outputDest, true).c_str(),
  898. NTV2AudioSystemToString(audioSystem, true).c_str(),
  899. NTV2VideoFormatToString(videoFormat, false).c_str(),
  900. NTV2FrameBufferFormatToString(pixelFormat, true).c_str());
  901. const NTV2DeviceID deviceID = card->GetDeviceID();
  902. if (GetIndexForNTV2Channel(outputProps.Channel()) > 0) {
  903. auto numFramestores = aja::CardNumFramestores(deviceID);
  904. for (UWord i = 0; i < numFramestores; i++) {
  905. auto channel = GetNTV2ChannelForIndex(i);
  906. if (cardEntry->ChannelReady(channel,
  907. ajaOutput->mOutputID)) {
  908. card->SetVideoFormat(videoFormat, false, false,
  909. channel);
  910. card->SetRegisterWriteMode(
  911. NTV2_REGWRITE_SYNCTOFRAME, channel);
  912. card->SetFrameBufferFormat(channel,
  913. pixelFormat);
  914. }
  915. }
  916. }
  917. // Configures crosspoint routing on AJA card
  918. if (!Routing::ConfigureOutputRoute(outputProps, NTV2_MODE_DISPLAY,
  919. card)) {
  920. blog(LOG_ERROR,
  921. "aja_output_start: Error configuring output route!");
  922. return false;
  923. }
  924. Routing::ConfigureOutputAudio(outputProps, card);
  925. const auto &formatDesc = outputProps.FormatDesc();
  926. struct video_scale_info scaler = {};
  927. scaler.format = aja::AJAPixelFormatToOBSVideoFormat(pixelFormat);
  928. scaler.width = formatDesc.GetRasterWidth();
  929. scaler.height = formatDesc.GetRasterHeight();
  930. // TODO(paulh): Find out what these scaler params actually do.
  931. // The colors are off when outputting the frames that OBS sends us.
  932. // but simply changing these values doesn't seem to have any effect.
  933. scaler.colorspace = VIDEO_CS_709;
  934. scaler.range = VIDEO_RANGE_PARTIAL;
  935. obs_output_set_video_conversion(ajaOutput->GetOBSOutput(), &scaler);
  936. struct audio_convert_info conversion = {};
  937. conversion.format = outputProps.AudioFormat();
  938. conversion.speakers = outputProps.SpeakerLayout();
  939. conversion.samples_per_sec = outputProps.audioSampleRate;
  940. obs_output_set_audio_conversion(ajaOutput->GetOBSOutput(), &conversion);
  941. if (!obs_output_begin_data_capture(ajaOutput->GetOBSOutput(), 0)) {
  942. blog(LOG_ERROR,
  943. "aja_output_start: Begin OBS data capture failed!");
  944. return false;
  945. }
  946. blog(LOG_INFO, "AJA Output started!");
  947. return true;
  948. }
  949. static void aja_output_stop(void *data, uint64_t ts)
  950. {
  951. UNUSED_PARAMETER(ts);
  952. blog(LOG_INFO, "Stopping AJA Output...");
  953. auto ajaOutput = (AJAOutput *)data;
  954. if (!ajaOutput) {
  955. blog(LOG_ERROR, "aja_output_stop: Plugin instance is null!");
  956. return;
  957. }
  958. auto outputProps = ajaOutput->GetOutputProps();
  959. const std::string &cardID = ajaOutput->mCardID;
  960. auto &cardManager = aja::CardManager::Instance();
  961. cardManager.EnumerateCards();
  962. auto cardEntry = cardManager.GetCardEntry(cardID);
  963. if (!cardEntry) {
  964. blog(LOG_ERROR, "aja_output_stop: Card Entry not found for %s",
  965. cardID.c_str());
  966. return;
  967. }
  968. CNTV2Card *card = ajaOutput->GetCard();
  969. if (!card) {
  970. blog(LOG_ERROR, "aja_output_stop: Card instance is null!");
  971. return;
  972. }
  973. if (!cardEntry->ReleaseOutputSelection(outputProps.ioSelect,
  974. card->GetDeviceID(),
  975. ajaOutput->mOutputID)) {
  976. blog(LOG_WARNING,
  977. "aja_output_stop: Error releasing IOSelection %s from card ID %s",
  978. aja::IOSelectionToString(outputProps.ioSelect).c_str(),
  979. cardID.c_str());
  980. }
  981. auto audioSystem = outputProps.AudioSystem();
  982. ajaOutput->GenerateTestPattern(outputProps.videoFormat,
  983. outputProps.pixelFormat,
  984. NTV2_TestPatt_Black);
  985. obs_output_end_data_capture(ajaOutput->GetOBSOutput());
  986. card->StopAudioOutput(audioSystem);
  987. blog(LOG_INFO, "AJA Output stopped.");
  988. }
  989. static void aja_output_raw_video(void *data, struct video_data *frame)
  990. {
  991. auto ajaOutput = (AJAOutput *)data;
  992. if (!ajaOutput)
  993. return;
  994. auto outputProps = ajaOutput->GetOutputProps();
  995. auto rasterBytes = outputProps.FormatDesc().GetTotalRasterBytes();
  996. ajaOutput->QueueVideoFrame(frame, rasterBytes);
  997. }
  998. static void aja_output_raw_audio(void *data, struct audio_data *frames)
  999. {
  1000. auto ajaOutput = (AJAOutput *)data;
  1001. if (!ajaOutput)
  1002. return;
  1003. auto outputProps = ajaOutput->GetOutputProps();
  1004. auto audioSize = outputProps.AudioSize();
  1005. auto audioBytes = static_cast<ULWord>(frames->frames * audioSize);
  1006. ajaOutput->QueueAudioFrames(frames, audioBytes);
  1007. }
  1008. static obs_properties_t *aja_output_get_properties(void *data)
  1009. {
  1010. obs_properties_t *props = obs_properties_create();
  1011. obs_property_t *device_list = obs_properties_add_list(
  1012. props, kUIPropDevice.id, obs_module_text(kUIPropDevice.text),
  1013. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
  1014. obs_property_t *output_list = obs_properties_add_list(
  1015. props, kUIPropOutput.id, obs_module_text(kUIPropOutput.text),
  1016. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  1017. obs_property_t *vid_fmt_list = obs_properties_add_list(
  1018. props, kUIPropVideoFormatSelect.id,
  1019. obs_module_text(kUIPropVideoFormatSelect.text),
  1020. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  1021. obs_properties_add_list(props, kUIPropPixelFormatSelect.id,
  1022. obs_module_text(kUIPropPixelFormatSelect.text),
  1023. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  1024. obs_properties_add_list(props, kUIPropSDI4KTransport.id,
  1025. obs_module_text(kUIPropSDI4KTransport.text),
  1026. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  1027. obs_properties_add_bool(props, kUIPropAutoStartOutput.id,
  1028. obs_module_text(kUIPropAutoStartOutput.text));
  1029. obs_property_set_modified_callback(vid_fmt_list,
  1030. aja_video_format_changed);
  1031. obs_property_set_modified_callback(output_list,
  1032. aja_output_dest_changed);
  1033. obs_property_set_modified_callback2(device_list,
  1034. aja_output_device_changed, data);
  1035. return props;
  1036. }
  1037. static const char *aja_output_get_name(void *)
  1038. {
  1039. return obs_module_text(kUIPropOutputModule.text);
  1040. }
  1041. // NOTE(paulh): Drop-down defaults are set on a clean launch in aja-output-ui code.
  1042. // Otherwise we load the settings stored in the ajaOutputProps/ajaPreviewOutputProps.json configs.
  1043. void aja_output_get_defaults(obs_data_t *settings)
  1044. {
  1045. obs_data_set_default_bool(settings, kUIPropAutoStartOutput.id, false);
  1046. }
  1047. struct obs_output_info create_aja_output_info()
  1048. {
  1049. struct obs_output_info aja_output_info = {};
  1050. aja_output_info.id = kUIPropOutputModule.id;
  1051. aja_output_info.flags = OBS_OUTPUT_AV;
  1052. aja_output_info.get_name = aja_output_get_name;
  1053. aja_output_info.create = aja_output_create;
  1054. aja_output_info.destroy = aja_output_destroy;
  1055. aja_output_info.start = aja_output_start;
  1056. aja_output_info.stop = aja_output_stop;
  1057. aja_output_info.raw_video = aja_output_raw_video;
  1058. aja_output_info.raw_audio = aja_output_raw_audio;
  1059. aja_output_info.update = aja_output_update;
  1060. aja_output_info.get_defaults = aja_output_get_defaults;
  1061. aja_output_info.get_properties = aja_output_get_properties;
  1062. return aja_output_info;
  1063. }