aja-output.cpp 36 KB

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