aja-output.cpp 39 KB

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