aja-source.cpp 34 KB

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