aja-source.cpp 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145
  1. #include "aja-card-manager.hpp"
  2. #include "aja-common.hpp"
  3. #include "aja-ui-props.hpp"
  4. #include "aja-source.hpp"
  5. #include "aja-routing.hpp"
  6. #include <util/threading.h>
  7. #include <util/platform.h>
  8. #include <util/dstr.h>
  9. #include <obs-module.h>
  10. #include <ajantv2/includes/ntv2card.h>
  11. #include <ajantv2/includes/ntv2utils.h>
  12. #define NTV2_AUDIOSIZE_MAX (401 * 1024)
  13. AJASource::AJASource(obs_source_t *source)
  14. : mVideoBuffer{},
  15. mAudioBuffer{},
  16. mCard{nullptr},
  17. mSourceName{""},
  18. mCardID{""},
  19. mDeviceIndex{0},
  20. mBuffering{false},
  21. mIsCapturing{false},
  22. mSourceProps{},
  23. mTestPattern{},
  24. mCaptureThread{nullptr},
  25. mMutex{},
  26. mSource{source}
  27. {
  28. }
  29. AJASource::~AJASource()
  30. {
  31. Deactivate();
  32. mTestPattern.clear();
  33. mVideoBuffer.Deallocate();
  34. mAudioBuffer.Deallocate();
  35. mVideoBuffer = NULL;
  36. mAudioBuffer = NULL;
  37. }
  38. void AJASource::SetCard(CNTV2Card *card)
  39. {
  40. mCard = card;
  41. }
  42. CNTV2Card *AJASource::GetCard()
  43. {
  44. return mCard;
  45. }
  46. void AJASource::SetOBSSource(obs_source_t *source)
  47. {
  48. mSource = source;
  49. }
  50. obs_source_t *AJASource::GetOBSSource(void) const
  51. {
  52. return mSource;
  53. }
  54. void AJASource::SetName(const std::string &name)
  55. {
  56. mSourceName = name;
  57. }
  58. std::string AJASource::GetName() const
  59. {
  60. return mSourceName;
  61. }
  62. void populate_source_device_list(obs_property_t *list)
  63. {
  64. obs_property_list_clear(list);
  65. auto &cardManager = aja::CardManager::Instance();
  66. cardManager.EnumerateCards();
  67. for (const auto &iter : cardManager.GetCardEntries()) {
  68. if (iter.second) {
  69. CNTV2Card *card = iter.second->GetCard();
  70. if (!card)
  71. continue;
  72. if (aja::IsOutputOnlyDevice(iter.second->GetDeviceID()))
  73. continue;
  74. obs_property_list_add_string(
  75. list, iter.second->GetDisplayName().c_str(),
  76. iter.second->GetCardID().c_str());
  77. }
  78. }
  79. }
  80. //
  81. // Capture Thread stuff
  82. //
  83. struct AudioOffsets {
  84. ULWord currentAddress = 0;
  85. ULWord lastAddress = 0;
  86. ULWord readOffset = 0;
  87. ULWord wrapAddress = 0;
  88. ULWord bytesRead = 0;
  89. };
  90. static void ResetAudioBufferOffsets(CNTV2Card *card,
  91. NTV2AudioSystem audioSystem,
  92. AudioOffsets &offsets)
  93. {
  94. if (!card)
  95. return;
  96. offsets.currentAddress = 0;
  97. offsets.lastAddress = 0;
  98. offsets.readOffset = 0;
  99. offsets.wrapAddress = 0;
  100. offsets.bytesRead = 0;
  101. card->GetAudioReadOffset(offsets.readOffset, audioSystem);
  102. card->GetAudioWrapAddress(offsets.wrapAddress, audioSystem);
  103. offsets.wrapAddress += offsets.readOffset;
  104. offsets.lastAddress = offsets.readOffset;
  105. }
  106. void AJASource::GenerateTestPattern(NTV2VideoFormat vf, NTV2PixelFormat pf,
  107. NTV2TestPatternSelect ps)
  108. {
  109. NTV2VideoFormat vid_fmt = vf;
  110. NTV2PixelFormat pix_fmt = pf;
  111. if (vid_fmt == NTV2_FORMAT_UNKNOWN)
  112. vid_fmt = NTV2_FORMAT_720p_5994;
  113. if (pix_fmt == NTV2_FBF_INVALID)
  114. pix_fmt = kDefaultAJAPixelFormat;
  115. NTV2FormatDesc fd(vid_fmt, pix_fmt, NTV2_VANCMODE_OFF);
  116. auto bufSize = fd.GetTotalRasterBytes();
  117. if (bufSize != mTestPattern.size()) {
  118. mTestPattern.clear();
  119. mTestPattern.resize(bufSize);
  120. NTV2TestPatternGen gen;
  121. gen.DrawTestPattern(ps, fd.GetRasterWidth(),
  122. fd.GetRasterHeight(), pix_fmt,
  123. mTestPattern);
  124. }
  125. if (mTestPattern.size() == 0) {
  126. blog(LOG_DEBUG,
  127. "AJASource::GenerateTestPattern: Error generating test pattern!");
  128. return;
  129. }
  130. struct obs_source_frame2 obsFrame;
  131. obsFrame.flip = false;
  132. obsFrame.timestamp = os_gettime_ns();
  133. obsFrame.width = fd.GetRasterWidth();
  134. obsFrame.height = fd.GetRasterHeight();
  135. obsFrame.format = aja::AJAPixelFormatToOBSVideoFormat(pix_fmt);
  136. obsFrame.data[0] = mTestPattern.data();
  137. obsFrame.linesize[0] = fd.GetBytesPerRow();
  138. video_format_get_parameters(VIDEO_CS_DEFAULT, VIDEO_RANGE_FULL,
  139. obsFrame.color_matrix,
  140. obsFrame.color_range_min,
  141. obsFrame.color_range_max);
  142. obs_source_output_video2(mSource, &obsFrame);
  143. blog(LOG_DEBUG, "AJASource::GenerateTestPattern: Black");
  144. }
  145. void AJASource::CaptureThread(AJAThread *thread, void *data)
  146. {
  147. UNUSED_PARAMETER(thread);
  148. auto ajaSource = (AJASource *)data;
  149. if (!ajaSource) {
  150. blog(LOG_WARNING,
  151. "AJASource::CaptureThread: Plugin instance is null!");
  152. return;
  153. }
  154. blog(LOG_INFO,
  155. "AJASource::CaptureThread: Starting capture thread for AJA source %s",
  156. ajaSource->GetName().c_str());
  157. auto card = ajaSource->GetCard();
  158. if (!card) {
  159. blog(LOG_ERROR,
  160. "AJASource::CaptureThread: Card instance is null!");
  161. return;
  162. }
  163. auto sourceProps = ajaSource->GetSourceProps();
  164. auto inputSource = sourceProps.InitialInputSource();
  165. auto channel = sourceProps.Channel();
  166. auto audioSystem = sourceProps.AudioSystem();
  167. // Current "on-air" frame on the card. The capture thread "Ping-pongs" between
  168. // two frames, starting at an index corresponding to the framestore channel.
  169. // For example:
  170. // Channel 1 (index 0) = frames 0/1
  171. // Channel 2 (index 1) = frames 2/3
  172. // Channel 3 (index 2) = frames 4/5
  173. // Channel 4 (index 3) = frames 6/7
  174. // etc...
  175. ULWord currentCardFrame = (uint32_t)channel * 2;
  176. card->WaitForInputFieldID(NTV2_FIELD0, channel);
  177. currentCardFrame ^= 1;
  178. card->SetInputFrame(channel, currentCardFrame);
  179. AudioOffsets offsets;
  180. ResetAudioBufferOffsets(card, audioSystem, offsets);
  181. obs_data_t *settings = obs_source_get_settings(ajaSource->mSource);
  182. while (ajaSource->IsCapturing()) {
  183. if (card->GetModelName() == "(Not Found)") {
  184. os_sleep_ms(250);
  185. obs_source_update(ajaSource->mSource, settings);
  186. break;
  187. }
  188. auto videoFormat = sourceProps.videoFormat;
  189. auto pixelFormat = sourceProps.pixelFormat;
  190. auto ioSelection = sourceProps.ioSelect;
  191. bool audioOverrun = false;
  192. card->WaitForInputFieldID(NTV2_FIELD0, channel);
  193. currentCardFrame ^= 1;
  194. // Card format detection -- restarts capture thread via aja_source_update callback
  195. auto newVideoFormat = card->GetInputVideoFormat(
  196. inputSource, aja::Is3GLevelB(card, channel));
  197. if (newVideoFormat == NTV2_FORMAT_UNKNOWN) {
  198. blog(LOG_DEBUG,
  199. "AJASource::CaptureThread: Video format unknown!");
  200. ajaSource->GenerateTestPattern(videoFormat, pixelFormat,
  201. NTV2_TestPatt_Black);
  202. os_sleep_ms(250);
  203. continue;
  204. }
  205. newVideoFormat = aja::HandleSpecialCaseFormats(
  206. ioSelection, newVideoFormat, sourceProps.deviceID);
  207. if (sourceProps.autoDetect && (videoFormat != newVideoFormat)) {
  208. blog(LOG_INFO,
  209. "AJASource::CaptureThread: New Video Format detected! Triggering 'aja_source_update' callback and returning...");
  210. blog(LOG_INFO,
  211. "AJASource::CaptureThread: Current Video Format: %s, | Want Video Format: %s",
  212. NTV2VideoFormatToString(videoFormat, true).c_str(),
  213. NTV2VideoFormatToString(newVideoFormat, true)
  214. .c_str());
  215. os_sleep_ms(250);
  216. obs_source_update(ajaSource->mSource, settings);
  217. break;
  218. }
  219. card->ReadAudioLastIn(offsets.currentAddress, audioSystem);
  220. offsets.currentAddress &= ~0x3; // Force DWORD alignment
  221. offsets.currentAddress += offsets.readOffset;
  222. if (offsets.currentAddress < offsets.lastAddress) {
  223. offsets.bytesRead =
  224. offsets.wrapAddress - offsets.lastAddress;
  225. if (offsets.bytesRead >
  226. ajaSource->mAudioBuffer.GetByteCount()) {
  227. blog(LOG_DEBUG,
  228. "AJASource::CaptureThread: Audio overrun (1)! Buffer Size: %d, Bytes Captured: %d",
  229. ajaSource->mAudioBuffer.GetByteCount(),
  230. offsets.bytesRead);
  231. ResetAudioBufferOffsets(card, audioSystem,
  232. offsets);
  233. audioOverrun = true;
  234. }
  235. if (!audioOverrun) {
  236. card->DMAReadAudio(audioSystem,
  237. ajaSource->mAudioBuffer,
  238. offsets.lastAddress,
  239. offsets.bytesRead);
  240. card->DMAReadAudio(
  241. audioSystem,
  242. reinterpret_cast<ULWord *>(
  243. ajaSource->mAudioBuffer
  244. .GetHostAddress(
  245. offsets.bytesRead)),
  246. offsets.readOffset,
  247. offsets.currentAddress -
  248. offsets.readOffset);
  249. offsets.bytesRead += offsets.currentAddress -
  250. offsets.readOffset;
  251. }
  252. if (offsets.bytesRead >
  253. ajaSource->mAudioBuffer.GetByteCount()) {
  254. blog(LOG_DEBUG,
  255. "AJASource::CaptureThread: Audio overrun (2)! Buffer Size: %d, Bytes Captured: %d",
  256. ajaSource->mAudioBuffer.GetByteCount(),
  257. offsets.bytesRead);
  258. ResetAudioBufferOffsets(card, audioSystem,
  259. offsets);
  260. audioOverrun = true;
  261. }
  262. } else {
  263. offsets.bytesRead =
  264. offsets.currentAddress - offsets.lastAddress;
  265. if (offsets.bytesRead >
  266. ajaSource->mAudioBuffer.GetByteCount()) {
  267. blog(LOG_DEBUG,
  268. "AJASource::CaptureThread: Audio overrun (3)! Buffer Size: %d, Bytes Captured: %d",
  269. ajaSource->mAudioBuffer.GetByteCount(),
  270. offsets.bytesRead);
  271. ResetAudioBufferOffsets(card, audioSystem,
  272. offsets);
  273. audioOverrun = true;
  274. }
  275. if (!audioOverrun) {
  276. card->DMAReadAudio(audioSystem,
  277. ajaSource->mAudioBuffer,
  278. offsets.lastAddress,
  279. offsets.bytesRead);
  280. }
  281. }
  282. if (!audioOverrun) {
  283. offsets.lastAddress = offsets.currentAddress;
  284. obs_source_audio audioPacket;
  285. audioPacket.samples_per_sec = 48000;
  286. audioPacket.format = AUDIO_FORMAT_32BIT;
  287. audioPacket.speakers = SPEAKERS_7POINT1;
  288. audioPacket.frames = offsets.bytesRead / 32;
  289. audioPacket.timestamp = os_gettime_ns();
  290. audioPacket.data[0] = (uint8_t *)ajaSource->mAudioBuffer
  291. .GetHostPointer();
  292. obs_source_output_audio(ajaSource->mSource,
  293. &audioPacket);
  294. }
  295. if (ajaSource->mVideoBuffer.GetByteCount() == 0) {
  296. blog(LOG_DEBUG,
  297. "AJASource::CaptureThread: 0 bytes in video buffer! Something went wrong!");
  298. continue;
  299. }
  300. card->DMAReadFrame(currentCardFrame, ajaSource->mVideoBuffer,
  301. ajaSource->mVideoBuffer.GetByteCount());
  302. auto actualVideoFormat = videoFormat;
  303. if (aja::Is3GLevelB(card, channel))
  304. actualVideoFormat = aja::GetLevelAFormatForLevelBFormat(
  305. videoFormat);
  306. NTV2FormatDesc fd(actualVideoFormat, pixelFormat);
  307. struct obs_source_frame2 obsFrame;
  308. obsFrame.flip = false;
  309. obsFrame.timestamp = os_gettime_ns();
  310. obsFrame.width = fd.GetRasterWidth();
  311. obsFrame.height = fd.GetRasterHeight();
  312. obsFrame.format = aja::AJAPixelFormatToOBSVideoFormat(
  313. sourceProps.pixelFormat);
  314. obsFrame.data[0] = reinterpret_cast<uint8_t *>(
  315. (ULWord *)ajaSource->mVideoBuffer.GetHostPointer());
  316. obsFrame.linesize[0] = fd.GetBytesPerRow();
  317. video_format_get_parameters(VIDEO_CS_DEFAULT, VIDEO_RANGE_FULL,
  318. obsFrame.color_matrix,
  319. obsFrame.color_range_min,
  320. obsFrame.color_range_max);
  321. obs_source_output_video2(ajaSource->mSource, &obsFrame);
  322. card->SetInputFrame(channel, currentCardFrame);
  323. }
  324. blog(LOG_INFO, "AJASource::Capturethread: Thread loop stopped");
  325. ajaSource->GenerateTestPattern(sourceProps.videoFormat,
  326. sourceProps.pixelFormat,
  327. NTV2_TestPatt_Black);
  328. obs_data_release(settings);
  329. }
  330. void AJASource::Deactivate()
  331. {
  332. SetCapturing(false);
  333. if (mCaptureThread) {
  334. if (mCaptureThread->Active()) {
  335. mCaptureThread->Stop();
  336. blog(LOG_INFO, "AJASource::CaptureThread: Stopped!");
  337. }
  338. delete mCaptureThread;
  339. mCaptureThread = nullptr;
  340. blog(LOG_INFO, "AJASource::CaptureThread: Destroyed!");
  341. }
  342. }
  343. void AJASource::Activate(bool enable)
  344. {
  345. if (mCaptureThread == nullptr) {
  346. mCaptureThread = new AJAThread();
  347. mCaptureThread->Attach(AJASource::CaptureThread, this);
  348. mCaptureThread->SetPriority(AJA_ThreadPriority_High);
  349. blog(LOG_INFO, "AJASource::CaptureThread: Created!");
  350. }
  351. if (enable) {
  352. SetCapturing(true);
  353. if (!mCaptureThread->Active()) {
  354. mCaptureThread->Start();
  355. blog(LOG_INFO, "AJASource::CaptureThread: Started!");
  356. }
  357. }
  358. }
  359. bool AJASource::IsCapturing() const
  360. {
  361. return mIsCapturing;
  362. }
  363. void AJASource::SetCapturing(bool capturing)
  364. {
  365. std::lock_guard<std::mutex> lock(mMutex);
  366. mIsCapturing = capturing;
  367. }
  368. //
  369. // CardEntry/Device stuff
  370. //
  371. std::string AJASource::CardID() const
  372. {
  373. return mCardID;
  374. }
  375. void AJASource::SetCardID(const std::string &cardID)
  376. {
  377. mCardID = cardID;
  378. }
  379. uint32_t AJASource::DeviceIndex() const
  380. {
  381. return static_cast<uint32_t>(mDeviceIndex);
  382. }
  383. void AJASource::SetDeviceIndex(uint32_t index)
  384. {
  385. mDeviceIndex = static_cast<UWord>(index);
  386. }
  387. //
  388. // AJASource Properties stuff
  389. //
  390. void AJASource::SetSourceProps(const SourceProps &props)
  391. {
  392. mSourceProps = props;
  393. }
  394. SourceProps AJASource::GetSourceProps() const
  395. {
  396. return mSourceProps;
  397. }
  398. bool AJASource::ReadChannelVPIDs(NTV2Channel channel, VPIDData &vpids)
  399. {
  400. ULWord vpid_a = 0;
  401. ULWord vpid_b = 0;
  402. bool read_ok = mCard->ReadSDIInVPID(channel, vpid_a, vpid_b);
  403. vpids.SetA(vpid_a);
  404. vpids.SetB(vpid_b);
  405. vpids.Parse();
  406. return read_ok;
  407. }
  408. bool AJASource::ReadWireFormats(NTV2DeviceID device_id, IOSelection io_select,
  409. NTV2VideoFormat &vf, NTV2PixelFormat &pf,
  410. VPIDDataList &vpids)
  411. {
  412. NTV2InputSourceSet input_srcs;
  413. aja::IOSelectionToInputSources(io_select, input_srcs);
  414. if (input_srcs.empty()) {
  415. blog(LOG_INFO,
  416. "AJASource::ReadWireFormats: No NTV2InputSources found for IOSelection %s",
  417. aja::IOSelectionToString(io_select).c_str());
  418. return false;
  419. }
  420. NTV2InputSource initial_src = *input_srcs.begin();
  421. for (auto &&src : input_srcs) {
  422. auto channel = NTV2InputSourceToChannel(src);
  423. mCard->EnableChannel(channel);
  424. if (NTV2_INPUT_SOURCE_IS_SDI(src)) {
  425. if (NTV2DeviceHasBiDirectionalSDI(device_id)) {
  426. mCard->SetSDITransmitEnable(channel, false);
  427. mCard->WaitForInputVerticalInterrupt(channel);
  428. VPIDData vpid_data;
  429. if (ReadChannelVPIDs(channel, vpid_data))
  430. vpids.push_back(vpid_data);
  431. }
  432. } else if (NTV2_INPUT_SOURCE_IS_HDMI(src)) {
  433. mCard->WaitForInputVerticalInterrupt(channel);
  434. ULWord hdmi_version =
  435. NTV2DeviceGetHDMIVersion(device_id);
  436. // HDMIv1 handles its own RGB->YCbCr color space conversion
  437. if (hdmi_version == 1) {
  438. pf = kDefaultAJAPixelFormat;
  439. } else {
  440. NTV2LHIHDMIColorSpace hdmiInputColor;
  441. mCard->GetHDMIInputColor(hdmiInputColor,
  442. channel);
  443. if (hdmiInputColor ==
  444. NTV2_LHIHDMIColorSpaceYCbCr) {
  445. pf = kDefaultAJAPixelFormat;
  446. } else if (hdmiInputColor ==
  447. NTV2_LHIHDMIColorSpaceRGB) {
  448. pf = NTV2_FBF_24BIT_BGR;
  449. }
  450. }
  451. }
  452. }
  453. NTV2Channel initial_channel = NTV2InputSourceToChannel(initial_src);
  454. mCard->WaitForInputVerticalInterrupt(initial_channel);
  455. vf = mCard->GetInputVideoFormat(
  456. initial_src, aja::Is3GLevelB(mCard, initial_channel));
  457. if (NTV2_INPUT_SOURCE_IS_SDI(initial_src)) {
  458. if (vpids.size() > 0) {
  459. auto vpid = *vpids.begin();
  460. if (vpid.Sampling() == VPIDSampling_YUV_422) {
  461. pf = NTV2_FBF_8BIT_YCBCR;
  462. blog(LOG_INFO,
  463. "AJASource::ReadWireFormats - Detected pixel format %s",
  464. NTV2FrameBufferFormatToString(pf, true)
  465. .c_str());
  466. } else if (vpid.Sampling() == VPIDSampling_GBR_444) {
  467. pf = NTV2_FBF_24BIT_BGR;
  468. blog(LOG_INFO,
  469. "AJASource::ReadWireFormats - Detected pixel format %s",
  470. NTV2FrameBufferFormatToString(pf, true)
  471. .c_str());
  472. }
  473. }
  474. }
  475. vf = aja::HandleSpecialCaseFormats(io_select, vf, device_id);
  476. blog(LOG_INFO, "AJASource::ReadWireFormats - Detected video format %s",
  477. NTV2VideoFormatToString(vf).c_str());
  478. return true;
  479. }
  480. void AJASource::ResetVideoBuffer(NTV2VideoFormat vf, NTV2PixelFormat pf)
  481. {
  482. if (vf != NTV2_FORMAT_UNKNOWN) {
  483. auto videoBufferSize = GetVideoWriteSize(vf, pf);
  484. if (mVideoBuffer)
  485. mVideoBuffer.Deallocate();
  486. mVideoBuffer.Allocate(videoBufferSize, true);
  487. blog(LOG_INFO,
  488. "AJASource::ResetVideoBuffer: Video Format: %s | Pixel Format: %s | Buffer Size: %d",
  489. NTV2VideoFormatToString(vf, false).c_str(),
  490. NTV2FrameBufferFormatToString(pf, true).c_str(),
  491. videoBufferSize);
  492. }
  493. }
  494. void AJASource::ResetAudioBuffer(size_t size)
  495. {
  496. if (mAudioBuffer)
  497. mAudioBuffer.Deallocate();
  498. mAudioBuffer.Allocate(size, true);
  499. }
  500. static const char *aja_source_get_name(void *);
  501. static void *aja_source_create(obs_data_t *, obs_source_t *);
  502. static void aja_source_destroy(void *);
  503. static void aja_source_activate(void *);
  504. static void aja_source_deactivate(void *);
  505. static void aja_source_update(void *, obs_data_t *);
  506. const char *aja_source_get_name(void *unused)
  507. {
  508. UNUSED_PARAMETER(unused);
  509. return obs_module_text(kUIPropCaptureModule.text);
  510. }
  511. bool aja_source_device_changed(void *data, obs_properties_t *props,
  512. obs_property_t *list, obs_data_t *settings)
  513. {
  514. UNUSED_PARAMETER(list);
  515. blog(LOG_DEBUG, "AJA Source Device Changed");
  516. auto *ajaSource = (AJASource *)data;
  517. if (!ajaSource) {
  518. blog(LOG_DEBUG,
  519. "aja_source_device_changed: AJA Source instance is null!");
  520. return false;
  521. }
  522. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  523. if (!cardID)
  524. return false;
  525. auto &cardManager = aja::CardManager::Instance();
  526. auto cardEntry = cardManager.GetCardEntry(cardID);
  527. if (!cardEntry) {
  528. blog(LOG_DEBUG,
  529. "aja_source_device_changed: Card Entry not found for %s",
  530. cardID);
  531. return false;
  532. }
  533. blog(LOG_DEBUG, "Found CardEntry for %s", cardID);
  534. CNTV2Card *card = cardEntry->GetCard();
  535. if (!card) {
  536. blog(LOG_DEBUG,
  537. "aja_source_device_changed: Card instance is null!");
  538. return false;
  539. }
  540. const NTV2DeviceID deviceID = card->GetDeviceID();
  541. /* If Channel 1 is actively in use, filter the video format list to only
  542. * show video formats within the same framerate family. If Channel 1 is
  543. * not active we just go ahead and try to set all framestores to the same video format.
  544. * This is because Channel 1's clock rate will govern the card's Free Run clock.
  545. */
  546. NTV2VideoFormat videoFormatChannel1 = NTV2_FORMAT_UNKNOWN;
  547. if (!cardEntry->ChannelReady(NTV2_CHANNEL1, ajaSource->GetName())) {
  548. card->GetVideoFormat(videoFormatChannel1, NTV2_CHANNEL1);
  549. }
  550. obs_property_t *devices_list =
  551. obs_properties_get(props, kUIPropDevice.id);
  552. obs_property_t *io_select_list =
  553. obs_properties_get(props, kUIPropInput.id);
  554. obs_property_t *vid_fmt_list =
  555. obs_properties_get(props, kUIPropVideoFormatSelect.id);
  556. obs_property_t *pix_fmt_list =
  557. obs_properties_get(props, kUIPropPixelFormatSelect.id);
  558. obs_property_t *sdi_trx_list =
  559. obs_properties_get(props, kUIPropSDITransport.id);
  560. obs_property_t *sdi_4k_list =
  561. obs_properties_get(props, kUIPropSDITransport4K.id);
  562. obs_property_list_clear(vid_fmt_list);
  563. obs_property_list_add_int(vid_fmt_list, obs_module_text("Auto"),
  564. kAutoDetect);
  565. populate_video_format_list(deviceID, vid_fmt_list, videoFormatChannel1);
  566. obs_property_list_clear(pix_fmt_list);
  567. obs_property_list_add_int(pix_fmt_list, obs_module_text("Auto"),
  568. kAutoDetect);
  569. populate_pixel_format_list(deviceID, pix_fmt_list);
  570. IOSelection io_select = static_cast<IOSelection>(
  571. obs_data_get_int(settings, kUIPropInput.id));
  572. obs_property_list_clear(sdi_trx_list);
  573. obs_property_list_add_int(sdi_trx_list, obs_module_text("Auto"),
  574. kAutoDetect);
  575. populate_sdi_transport_list(sdi_trx_list, io_select);
  576. obs_property_list_clear(sdi_4k_list);
  577. populate_sdi_4k_transport_list(sdi_4k_list);
  578. populate_io_selection_input_list(cardID, ajaSource->GetName(), deviceID,
  579. io_select_list);
  580. auto curr_vf = static_cast<NTV2VideoFormat>(
  581. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  582. bool have_cards = cardManager.NumCardEntries() > 0;
  583. obs_property_set_visible(devices_list, have_cards);
  584. obs_property_set_visible(io_select_list, have_cards);
  585. obs_property_set_visible(vid_fmt_list, have_cards);
  586. obs_property_set_visible(pix_fmt_list, have_cards);
  587. obs_property_set_visible(
  588. sdi_4k_list, have_cards && NTV2_IS_4K_VIDEO_FORMAT(curr_vf));
  589. return true;
  590. }
  591. bool aja_io_selection_changed(void *data, obs_properties_t *props,
  592. obs_property_t *list, obs_data_t *settings)
  593. {
  594. UNUSED_PARAMETER(list);
  595. AJASource *ajaSource = (AJASource *)data;
  596. if (!ajaSource) {
  597. blog(LOG_DEBUG,
  598. "aja_io_selection_changed: AJA Source instance is null!");
  599. return false;
  600. }
  601. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  602. if (!cardID)
  603. return false;
  604. auto &cardManager = aja::CardManager::Instance();
  605. auto cardEntry = cardManager.GetCardEntry(cardID);
  606. if (!cardEntry) {
  607. blog(LOG_DEBUG,
  608. "aja_io_selection_changed: Card Entry not found for %s",
  609. cardID);
  610. return false;
  611. }
  612. obs_property_t *io_select_list =
  613. obs_properties_get(props, kUIPropInput.id);
  614. filter_io_selection_input_list(cardID, ajaSource->GetName(),
  615. io_select_list);
  616. return true;
  617. }
  618. bool aja_sdi_mode_list_changed(obs_properties_t *props, obs_property_t *list,
  619. obs_data_t *settings)
  620. {
  621. UNUSED_PARAMETER(props);
  622. UNUSED_PARAMETER(list);
  623. UNUSED_PARAMETER(settings);
  624. return true;
  625. }
  626. void *aja_source_create(obs_data_t *settings, obs_source_t *source)
  627. {
  628. blog(LOG_DEBUG, "AJA Source Create");
  629. auto ajaSource = new AJASource(source);
  630. ajaSource->SetName(obs_source_get_name(source));
  631. obs_source_set_async_decoupled(source, true);
  632. ajaSource->SetOBSSource(source);
  633. ajaSource->ResetAudioBuffer(NTV2_AUDIOSIZE_MAX);
  634. ajaSource->Activate(false);
  635. obs_source_update(source, settings);
  636. return ajaSource;
  637. }
  638. void aja_source_destroy(void *data)
  639. {
  640. blog(LOG_DEBUG, "AJA Source Destroy");
  641. auto ajaSource = (AJASource *)data;
  642. if (!ajaSource) {
  643. blog(LOG_ERROR, "aja_source_destroy: Plugin instance is null!");
  644. return;
  645. }
  646. ajaSource->Deactivate();
  647. NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
  648. CNTV2Card *card = ajaSource->GetCard();
  649. if (card) {
  650. deviceID = card->GetDeviceID();
  651. aja::Routing::StopSourceAudio(ajaSource->GetSourceProps(),
  652. card);
  653. }
  654. ajaSource->mVideoBuffer.Deallocate();
  655. ajaSource->mAudioBuffer.Deallocate();
  656. ajaSource->mVideoBuffer = NULL;
  657. ajaSource->mAudioBuffer = NULL;
  658. auto &cardManager = aja::CardManager::Instance();
  659. const auto &cardID = ajaSource->CardID();
  660. auto cardEntry = cardManager.GetCardEntry(cardID);
  661. if (!cardEntry) {
  662. blog(LOG_DEBUG,
  663. "aja_source_destroy: Card Entry not found for %s",
  664. cardID.c_str());
  665. return;
  666. }
  667. auto ioSelect = ajaSource->GetSourceProps().ioSelect;
  668. if (!cardEntry->ReleaseInputSelection(ioSelect, deviceID,
  669. ajaSource->GetName())) {
  670. blog(LOG_WARNING,
  671. "aja_source_destroy: Error releasing Input Selection!");
  672. }
  673. delete ajaSource;
  674. ajaSource = nullptr;
  675. }
  676. static void aja_source_show(void *data)
  677. {
  678. auto ajaSource = (AJASource *)data;
  679. if (!ajaSource) {
  680. blog(LOG_ERROR,
  681. "aja_source_show: AJA Source instance is null!");
  682. return;
  683. }
  684. bool deactivateWhileNotShowing =
  685. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  686. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  687. blog(LOG_DEBUG,
  688. "aja_source_show: deactivateWhileNotShowing = %s, showing = %s",
  689. deactivateWhileNotShowing ? "true" : "false",
  690. showing ? "true" : "false");
  691. if (deactivateWhileNotShowing && showing && !ajaSource->IsCapturing()) {
  692. ajaSource->Activate(true);
  693. blog(LOG_DEBUG, "aja_source_show: activated capture thread!");
  694. }
  695. }
  696. static void aja_source_hide(void *data)
  697. {
  698. auto ajaSource = (AJASource *)data;
  699. if (!ajaSource)
  700. return;
  701. bool deactivateWhileNotShowing =
  702. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  703. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  704. blog(LOG_DEBUG,
  705. "aja_source_hide: deactivateWhileNotShowing = %s, showing = %s",
  706. deactivateWhileNotShowing ? "true" : "false",
  707. showing ? "true" : "false");
  708. if (deactivateWhileNotShowing && !showing && ajaSource->IsCapturing()) {
  709. ajaSource->Deactivate();
  710. blog(LOG_DEBUG, "aja_source_hide: deactivated capture thread!");
  711. }
  712. }
  713. static void aja_source_activate(void *data)
  714. {
  715. UNUSED_PARAMETER(data);
  716. }
  717. static void aja_source_deactivate(void *data)
  718. {
  719. UNUSED_PARAMETER(data);
  720. }
  721. static void aja_source_update(void *data, obs_data_t *settings)
  722. {
  723. static bool initialized = false;
  724. auto ajaSource = (AJASource *)data;
  725. if (!ajaSource) {
  726. blog(LOG_WARNING,
  727. "aja_source_update: Plugin instance is null!");
  728. return;
  729. }
  730. auto io_select = static_cast<IOSelection>(
  731. obs_data_get_int(settings, kUIPropInput.id));
  732. auto vf_select = static_cast<NTV2VideoFormat>(
  733. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  734. auto pf_select = static_cast<NTV2PixelFormat>(
  735. obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
  736. auto sdi_trx_select = static_cast<SDITransport>(
  737. obs_data_get_int(settings, kUIPropSDITransport.id));
  738. auto sdi_t4k_select = static_cast<SDITransport4K>(
  739. obs_data_get_int(settings, kUIPropSDITransport4K.id));
  740. bool deactivateWhileNotShowing =
  741. obs_data_get_bool(settings, kUIPropDeactivateWhenNotShowing.id);
  742. const std::string &wantCardID =
  743. obs_data_get_string(settings, kUIPropDevice.id);
  744. const std::string &currentCardID = ajaSource->CardID();
  745. if (wantCardID != currentCardID) {
  746. initialized = false;
  747. ajaSource->Deactivate();
  748. }
  749. auto &cardManager = aja::CardManager::Instance();
  750. cardManager.EnumerateCards();
  751. auto cardEntry = cardManager.GetCardEntry(wantCardID);
  752. if (!cardEntry) {
  753. blog(LOG_DEBUG,
  754. "aja_source_update: Card Entry not found for %s",
  755. wantCardID.c_str());
  756. return;
  757. }
  758. CNTV2Card *card = cardEntry->GetCard();
  759. if (!card || !card->IsOpen()) {
  760. blog(LOG_ERROR, "aja_source_update: AJA device %s not open!",
  761. wantCardID.c_str());
  762. return;
  763. }
  764. if (card->GetModelName() == "(Not Found)") {
  765. blog(LOG_ERROR,
  766. "aja_source_update: AJA device %s disconnected?",
  767. wantCardID.c_str());
  768. return;
  769. }
  770. ajaSource->SetCard(cardEntry->GetCard());
  771. SourceProps curr_props = ajaSource->GetSourceProps();
  772. // Release Channels from previous card if card ID changes
  773. if (wantCardID != currentCardID) {
  774. auto prevCardEntry = cardManager.GetCardEntry(currentCardID);
  775. if (prevCardEntry) {
  776. const std::string &ioSelectStr =
  777. aja::IOSelectionToString(curr_props.ioSelect);
  778. if (!prevCardEntry->ReleaseInputSelection(
  779. curr_props.ioSelect, curr_props.deviceID,
  780. ajaSource->GetName())) {
  781. blog(LOG_WARNING,
  782. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  783. ioSelectStr.c_str(),
  784. currentCardID.c_str());
  785. } else {
  786. blog(LOG_INFO,
  787. "aja_source_update: Released IOSelection %s for card ID %s",
  788. ioSelectStr.c_str(),
  789. currentCardID.c_str());
  790. ajaSource->SetCardID(wantCardID);
  791. io_select = IOSelection::Invalid;
  792. }
  793. }
  794. }
  795. if (io_select == IOSelection::Invalid) {
  796. blog(LOG_DEBUG, "aja_source_update: Invalid IOSelection");
  797. return;
  798. }
  799. SourceProps want_props;
  800. want_props.deviceID = card->GetDeviceID();
  801. want_props.ioSelect = io_select;
  802. want_props.videoFormat =
  803. ((int32_t)vf_select == kAutoDetect)
  804. ? NTV2_FORMAT_UNKNOWN
  805. : static_cast<NTV2VideoFormat>(vf_select);
  806. want_props.pixelFormat =
  807. ((int32_t)pf_select == kAutoDetect)
  808. ? NTV2_FBF_INVALID
  809. : static_cast<NTV2PixelFormat>(pf_select);
  810. want_props.sdiTransport =
  811. ((int32_t)sdi_trx_select == kAutoDetect)
  812. ? SDITransport::Unknown
  813. : static_cast<SDITransport>(sdi_trx_select);
  814. want_props.sdi4kTransport = sdi_t4k_select;
  815. want_props.vpids.clear();
  816. want_props.deactivateWhileNotShowing = deactivateWhileNotShowing;
  817. want_props.autoDetect = ((int32_t)vf_select == kAutoDetect ||
  818. (int32_t)pf_select == kAutoDetect);
  819. ajaSource->SetCardID(wantCardID);
  820. ajaSource->SetDeviceIndex((UWord)cardEntry->GetCardIndex());
  821. if (NTV2_IS_4K_VIDEO_FORMAT(want_props.videoFormat) &&
  822. want_props.sdi4kTransport == SDITransport4K::Squares) {
  823. if (want_props.ioSelect == IOSelection::SDI1_2) {
  824. want_props.ioSelect = IOSelection::SDI1_2_Squares;
  825. } else if (want_props.ioSelect == IOSelection::SDI3_4) {
  826. want_props.ioSelect = IOSelection::SDI3_4_Squares;
  827. }
  828. }
  829. // Release Channels if IOSelection changes
  830. if (want_props.ioSelect != curr_props.ioSelect) {
  831. const std::string &ioSelectStr =
  832. aja::IOSelectionToString(curr_props.ioSelect);
  833. if (!cardEntry->ReleaseInputSelection(curr_props.ioSelect,
  834. curr_props.deviceID,
  835. ajaSource->GetName())) {
  836. blog(LOG_WARNING,
  837. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  838. ioSelectStr.c_str(), currentCardID.c_str());
  839. } else {
  840. blog(LOG_INFO,
  841. "aja_source_update: Released IOSelection %s for card ID %s",
  842. ioSelectStr.c_str(), currentCardID.c_str());
  843. }
  844. }
  845. // Acquire Channels for current IOSelection
  846. if (!cardEntry->AcquireInputSelection(want_props.ioSelect,
  847. want_props.deviceID,
  848. ajaSource->GetName())) {
  849. blog(LOG_ERROR,
  850. "aja_source_update: Could not acquire IOSelection %s",
  851. aja::IOSelectionToString(want_props.ioSelect).c_str());
  852. return;
  853. }
  854. // Read SDI video payload IDs (VPID) used for helping to determine the wire format
  855. NTV2VideoFormat new_vf = want_props.videoFormat;
  856. NTV2PixelFormat new_pf = want_props.pixelFormat;
  857. if (!ajaSource->ReadWireFormats(want_props.deviceID,
  858. want_props.ioSelect, new_vf, new_pf,
  859. want_props.vpids)) {
  860. blog(LOG_ERROR, "aja_source_update: ReadWireFormats failed!");
  861. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  862. curr_props.deviceID,
  863. ajaSource->GetName());
  864. return;
  865. }
  866. // Set auto-detected formats
  867. if ((int32_t)vf_select == kAutoDetect)
  868. want_props.videoFormat = new_vf;
  869. if ((int32_t)pf_select == kAutoDetect)
  870. want_props.pixelFormat = new_pf;
  871. if (want_props.videoFormat == NTV2_FORMAT_UNKNOWN ||
  872. want_props.pixelFormat == NTV2_FBF_INVALID) {
  873. blog(LOG_ERROR,
  874. "aja_source_update: Unknown video/pixel format(s): %s / %s",
  875. NTV2VideoFormatToString(want_props.videoFormat).c_str(),
  876. NTV2FrameBufferFormatToString(want_props.pixelFormat)
  877. .c_str());
  878. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  879. curr_props.deviceID,
  880. ajaSource->GetName());
  881. return;
  882. }
  883. // Change capture format and restart capture thread
  884. if (!initialized || want_props != ajaSource->GetSourceProps()) {
  885. aja::Routing::ConfigureSourceRoute(want_props,
  886. NTV2_MODE_CAPTURE, card);
  887. ajaSource->Deactivate();
  888. initialized = true;
  889. }
  890. ajaSource->SetSourceProps(want_props);
  891. ajaSource->ResetVideoBuffer(want_props.videoFormat,
  892. want_props.pixelFormat);
  893. aja::Routing::StartSourceAudio(want_props, card);
  894. card->SetReference(NTV2_REFERENCE_FREERUN);
  895. ajaSource->Activate(true);
  896. }
  897. static obs_properties_t *aja_source_get_properties(void *data)
  898. {
  899. obs_properties_t *props = obs_properties_create();
  900. obs_property_t *device_list = obs_properties_add_list(
  901. props, kUIPropDevice.id, obs_module_text(kUIPropDevice.text),
  902. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
  903. populate_source_device_list(device_list);
  904. obs_property_t *io_select_list = obs_properties_add_list(
  905. props, kUIPropInput.id, obs_module_text(kUIPropInput.text),
  906. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  907. obs_property_t *vid_fmt_list = obs_properties_add_list(
  908. props, kUIPropVideoFormatSelect.id,
  909. obs_module_text(kUIPropVideoFormatSelect.text),
  910. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  911. obs_properties_add_list(props, kUIPropPixelFormatSelect.id,
  912. obs_module_text(kUIPropPixelFormatSelect.text),
  913. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  914. obs_properties_add_list(props, kUIPropSDITransport.id,
  915. obs_module_text(kUIPropSDITransport.text),
  916. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  917. obs_properties_add_list(props, kUIPropSDITransport4K.id,
  918. obs_module_text(kUIPropSDITransport4K.text),
  919. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  920. obs_properties_add_bool(
  921. props, kUIPropDeactivateWhenNotShowing.id,
  922. obs_module_text(kUIPropDeactivateWhenNotShowing.text));
  923. obs_property_set_modified_callback(vid_fmt_list,
  924. aja_video_format_changed);
  925. obs_property_set_modified_callback2(device_list,
  926. aja_source_device_changed, data);
  927. obs_property_set_modified_callback2(io_select_list,
  928. aja_io_selection_changed, data);
  929. return props;
  930. }
  931. void aja_source_get_defaults(obs_data_t *settings)
  932. {
  933. obs_data_set_default_int(settings, kUIPropInput.id,
  934. static_cast<long long>(IOSelection::Invalid));
  935. obs_data_set_default_int(settings, kUIPropVideoFormatSelect.id,
  936. static_cast<long long>(kAutoDetect));
  937. obs_data_set_default_int(settings, kUIPropPixelFormatSelect.id,
  938. static_cast<long long>(kAutoDetect));
  939. obs_data_set_default_int(settings, kUIPropSDITransport.id,
  940. static_cast<long long>(kAutoDetect));
  941. obs_data_set_default_int(
  942. settings, kUIPropSDITransport4K.id,
  943. static_cast<long long>(SDITransport4K::TwoSampleInterleave));
  944. obs_data_set_default_bool(settings, kUIPropDeactivateWhenNotShowing.id,
  945. false);
  946. }
  947. void aja_source_save(void *data, obs_data_t *settings)
  948. {
  949. AJASource *ajaSource = (AJASource *)data;
  950. if (!ajaSource) {
  951. blog(LOG_ERROR,
  952. "aja_source_save: AJA Source instance is null!");
  953. return;
  954. }
  955. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  956. if (!cardID)
  957. return;
  958. auto &cardManager = aja::CardManager::Instance();
  959. auto cardEntry = cardManager.GetCardEntry(cardID);
  960. if (!cardEntry) {
  961. blog(LOG_DEBUG, "aja_source_save: Card Entry not found for %s",
  962. cardID);
  963. return;
  964. }
  965. auto oldName = ajaSource->GetName();
  966. auto newName = obs_source_get_name(ajaSource->GetOBSSource());
  967. if (oldName != newName &&
  968. cardEntry->UpdateChannelOwnerName(oldName, newName)) {
  969. ajaSource->SetName(newName);
  970. blog(LOG_DEBUG, "aja_source_save: Renamed \"%s\" to \"%s\"",
  971. oldName.c_str(), newName);
  972. }
  973. }
  974. struct obs_source_info create_aja_source_info()
  975. {
  976. struct obs_source_info aja_source_info = {};
  977. aja_source_info.id = kUIPropCaptureModule.id;
  978. aja_source_info.type = OBS_SOURCE_TYPE_INPUT;
  979. aja_source_info.output_flags = OBS_SOURCE_ASYNC_VIDEO |
  980. OBS_SOURCE_AUDIO |
  981. OBS_SOURCE_DO_NOT_DUPLICATE;
  982. aja_source_info.get_name = aja_source_get_name;
  983. aja_source_info.create = aja_source_create;
  984. aja_source_info.destroy = aja_source_destroy;
  985. aja_source_info.update = aja_source_update;
  986. aja_source_info.show = aja_source_show;
  987. aja_source_info.hide = aja_source_hide;
  988. aja_source_info.activate = aja_source_activate;
  989. aja_source_info.deactivate = aja_source_deactivate;
  990. aja_source_info.get_properties = aja_source_get_properties;
  991. aja_source_info.get_defaults = aja_source_get_defaults;
  992. aja_source_info.save = aja_source_save;
  993. aja_source_info.icon_type = OBS_ICON_TYPE_CAMERA;
  994. return aja_source_info;
  995. }