aja-source.cpp 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  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. obs_property_list_clear(pix_fmt_list);
  582. obs_property_list_add_int(pix_fmt_list, obs_module_text("Auto"),
  583. kAutoDetect);
  584. populate_pixel_format_list(deviceID, pix_fmt_list);
  585. IOSelection io_select = static_cast<IOSelection>(
  586. obs_data_get_int(settings, kUIPropInput.id));
  587. obs_property_list_clear(sdi_trx_list);
  588. obs_property_list_add_int(sdi_trx_list, obs_module_text("Auto"),
  589. kAutoDetect);
  590. populate_sdi_transport_list(sdi_trx_list, io_select);
  591. obs_property_list_clear(sdi_4k_list);
  592. populate_sdi_4k_transport_list(sdi_4k_list);
  593. populate_io_selection_input_list(cardID, ajaSource->GetName(), deviceID,
  594. io_select_list);
  595. auto curr_vf = static_cast<NTV2VideoFormat>(
  596. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  597. bool have_cards = cardManager.NumCardEntries() > 0;
  598. obs_property_set_visible(devices_list, have_cards);
  599. obs_property_set_visible(io_select_list, have_cards);
  600. obs_property_set_visible(vid_fmt_list, have_cards);
  601. obs_property_set_visible(pix_fmt_list, have_cards);
  602. obs_property_set_visible(
  603. sdi_4k_list, have_cards && NTV2_IS_4K_VIDEO_FORMAT(curr_vf));
  604. return true;
  605. }
  606. bool aja_io_selection_changed(void *data, obs_properties_t *props,
  607. obs_property_t *list, obs_data_t *settings)
  608. {
  609. UNUSED_PARAMETER(list);
  610. AJASource *ajaSource = (AJASource *)data;
  611. if (!ajaSource) {
  612. blog(LOG_DEBUG,
  613. "aja_io_selection_changed: AJA Source instance is null!");
  614. return false;
  615. }
  616. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  617. if (!cardID || !cardID[0])
  618. return false;
  619. auto &cardManager = aja::CardManager::Instance();
  620. auto cardEntry = cardManager.GetCardEntry(cardID);
  621. if (!cardEntry) {
  622. blog(LOG_DEBUG,
  623. "aja_io_selection_changed: Card Entry not found for %s",
  624. cardID);
  625. return false;
  626. }
  627. obs_property_t *io_select_list =
  628. obs_properties_get(props, kUIPropInput.id);
  629. filter_io_selection_input_list(cardID, ajaSource->GetName(),
  630. io_select_list);
  631. return true;
  632. }
  633. bool aja_sdi_mode_list_changed(obs_properties_t *props, obs_property_t *list,
  634. obs_data_t *settings)
  635. {
  636. UNUSED_PARAMETER(props);
  637. UNUSED_PARAMETER(list);
  638. UNUSED_PARAMETER(settings);
  639. return true;
  640. }
  641. void *aja_source_create(obs_data_t *settings, obs_source_t *source)
  642. {
  643. blog(LOG_DEBUG, "AJA Source Create");
  644. auto ajaSource = new AJASource(source);
  645. ajaSource->SetName(obs_source_get_name(source));
  646. obs_source_set_async_decoupled(source, true);
  647. ajaSource->SetOBSSource(source);
  648. ajaSource->ResetAudioBuffer(NTV2_AUDIOSIZE_MAX);
  649. ajaSource->Activate(false);
  650. obs_source_update(source, settings);
  651. return ajaSource;
  652. }
  653. void aja_source_destroy(void *data)
  654. {
  655. blog(LOG_DEBUG, "AJA Source Destroy");
  656. auto ajaSource = (AJASource *)data;
  657. if (!ajaSource) {
  658. blog(LOG_ERROR, "aja_source_destroy: Plugin instance is null!");
  659. return;
  660. }
  661. ajaSource->Deactivate();
  662. NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
  663. CNTV2Card *card = ajaSource->GetCard();
  664. if (card) {
  665. deviceID = card->GetDeviceID();
  666. aja::Routing::StopSourceAudio(ajaSource->GetSourceProps(),
  667. card);
  668. }
  669. ajaSource->mVideoBuffer.Deallocate();
  670. ajaSource->mAudioBuffer.Deallocate();
  671. ajaSource->mVideoBuffer = NULL;
  672. ajaSource->mAudioBuffer = NULL;
  673. auto &cardManager = aja::CardManager::Instance();
  674. const auto &cardID = ajaSource->CardID();
  675. auto cardEntry = cardManager.GetCardEntry(cardID);
  676. if (!cardEntry) {
  677. blog(LOG_DEBUG,
  678. "aja_source_destroy: Card Entry not found for %s",
  679. cardID.c_str());
  680. return;
  681. }
  682. auto ioSelect = ajaSource->GetSourceProps().ioSelect;
  683. if (!cardEntry->ReleaseInputSelection(ioSelect, deviceID,
  684. ajaSource->GetName())) {
  685. blog(LOG_WARNING,
  686. "aja_source_destroy: Error releasing Input Selection!");
  687. }
  688. delete ajaSource;
  689. ajaSource = nullptr;
  690. }
  691. static void aja_source_show(void *data)
  692. {
  693. auto ajaSource = (AJASource *)data;
  694. if (!ajaSource) {
  695. blog(LOG_ERROR,
  696. "aja_source_show: AJA Source instance is null!");
  697. return;
  698. }
  699. bool deactivateWhileNotShowing =
  700. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  701. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  702. blog(LOG_DEBUG,
  703. "aja_source_show: deactivateWhileNotShowing = %s, showing = %s",
  704. deactivateWhileNotShowing ? "true" : "false",
  705. showing ? "true" : "false");
  706. if (deactivateWhileNotShowing && showing && !ajaSource->IsCapturing()) {
  707. ajaSource->Activate(true);
  708. blog(LOG_DEBUG, "aja_source_show: activated capture thread!");
  709. }
  710. }
  711. static void aja_source_hide(void *data)
  712. {
  713. auto ajaSource = (AJASource *)data;
  714. if (!ajaSource)
  715. return;
  716. bool deactivateWhileNotShowing =
  717. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  718. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  719. blog(LOG_DEBUG,
  720. "aja_source_hide: deactivateWhileNotShowing = %s, showing = %s",
  721. deactivateWhileNotShowing ? "true" : "false",
  722. showing ? "true" : "false");
  723. if (deactivateWhileNotShowing && !showing && ajaSource->IsCapturing()) {
  724. ajaSource->Deactivate();
  725. blog(LOG_DEBUG, "aja_source_hide: deactivated capture thread!");
  726. }
  727. }
  728. static void aja_source_activate(void *data)
  729. {
  730. UNUSED_PARAMETER(data);
  731. }
  732. static void aja_source_deactivate(void *data)
  733. {
  734. UNUSED_PARAMETER(data);
  735. }
  736. static void aja_source_update(void *data, obs_data_t *settings)
  737. {
  738. static bool initialized = false;
  739. auto ajaSource = (AJASource *)data;
  740. if (!ajaSource) {
  741. blog(LOG_WARNING,
  742. "aja_source_update: Plugin instance is null!");
  743. return;
  744. }
  745. auto io_select = static_cast<IOSelection>(
  746. obs_data_get_int(settings, kUIPropInput.id));
  747. auto vf_select = static_cast<NTV2VideoFormat>(
  748. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  749. auto pf_select = static_cast<NTV2PixelFormat>(
  750. obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
  751. auto sdi_trx_select = static_cast<SDITransport>(
  752. obs_data_get_int(settings, kUIPropSDITransport.id));
  753. auto sdi_t4k_select = static_cast<SDITransport4K>(
  754. obs_data_get_int(settings, kUIPropSDITransport4K.id));
  755. bool deactivateWhileNotShowing =
  756. obs_data_get_bool(settings, kUIPropDeactivateWhenNotShowing.id);
  757. const std::string &wantCardID =
  758. obs_data_get_string(settings, kUIPropDevice.id);
  759. const std::string &currentCardID = ajaSource->CardID();
  760. if (wantCardID != currentCardID) {
  761. initialized = false;
  762. ajaSource->Deactivate();
  763. }
  764. auto &cardManager = aja::CardManager::Instance();
  765. cardManager.EnumerateCards();
  766. auto cardEntry = cardManager.GetCardEntry(wantCardID);
  767. if (!cardEntry) {
  768. blog(LOG_DEBUG,
  769. "aja_source_update: Card Entry not found for %s",
  770. wantCardID.c_str());
  771. return;
  772. }
  773. CNTV2Card *card = cardEntry->GetCard();
  774. if (!card || !card->IsOpen()) {
  775. blog(LOG_ERROR, "aja_source_update: AJA device %s not open!",
  776. wantCardID.c_str());
  777. return;
  778. }
  779. if (card->GetModelName() == "(Not Found)") {
  780. blog(LOG_ERROR,
  781. "aja_source_update: AJA device %s disconnected?",
  782. wantCardID.c_str());
  783. return;
  784. }
  785. ajaSource->SetCard(cardEntry->GetCard());
  786. SourceProps curr_props = ajaSource->GetSourceProps();
  787. // Release Channels from previous card if card ID changes
  788. if (wantCardID != currentCardID) {
  789. auto prevCardEntry = cardManager.GetCardEntry(currentCardID);
  790. if (prevCardEntry) {
  791. const std::string &ioSelectStr =
  792. aja::IOSelectionToString(curr_props.ioSelect);
  793. if (!prevCardEntry->ReleaseInputSelection(
  794. curr_props.ioSelect, curr_props.deviceID,
  795. ajaSource->GetName())) {
  796. blog(LOG_WARNING,
  797. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  798. ioSelectStr.c_str(),
  799. currentCardID.c_str());
  800. } else {
  801. blog(LOG_INFO,
  802. "aja_source_update: Released IOSelection %s for card ID %s",
  803. ioSelectStr.c_str(),
  804. currentCardID.c_str());
  805. ajaSource->SetCardID(wantCardID);
  806. io_select = IOSelection::Invalid;
  807. }
  808. }
  809. }
  810. if (io_select == IOSelection::Invalid) {
  811. blog(LOG_DEBUG, "aja_source_update: Invalid IOSelection");
  812. return;
  813. }
  814. SourceProps want_props;
  815. want_props.deviceID = card->GetDeviceID();
  816. want_props.ioSelect = io_select;
  817. want_props.videoFormat =
  818. ((int32_t)vf_select == kAutoDetect)
  819. ? NTV2_FORMAT_UNKNOWN
  820. : static_cast<NTV2VideoFormat>(vf_select);
  821. want_props.pixelFormat =
  822. ((int32_t)pf_select == kAutoDetect)
  823. ? NTV2_FBF_INVALID
  824. : static_cast<NTV2PixelFormat>(pf_select);
  825. want_props.sdiTransport =
  826. ((int32_t)sdi_trx_select == kAutoDetect)
  827. ? SDITransport::Unknown
  828. : static_cast<SDITransport>(sdi_trx_select);
  829. want_props.sdi4kTransport = sdi_t4k_select;
  830. want_props.vpids.clear();
  831. want_props.deactivateWhileNotShowing = deactivateWhileNotShowing;
  832. want_props.autoDetect = ((int32_t)vf_select == kAutoDetect ||
  833. (int32_t)pf_select == kAutoDetect);
  834. ajaSource->SetCardID(wantCardID);
  835. ajaSource->SetDeviceIndex((UWord)cardEntry->GetCardIndex());
  836. // Release Channels if IOSelection changes
  837. if (want_props.ioSelect != curr_props.ioSelect) {
  838. const std::string &ioSelectStr =
  839. aja::IOSelectionToString(curr_props.ioSelect);
  840. if (!cardEntry->ReleaseInputSelection(curr_props.ioSelect,
  841. curr_props.deviceID,
  842. ajaSource->GetName())) {
  843. blog(LOG_WARNING,
  844. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  845. ioSelectStr.c_str(), currentCardID.c_str());
  846. } else {
  847. blog(LOG_INFO,
  848. "aja_source_update: Released IOSelection %s for card ID %s",
  849. ioSelectStr.c_str(), currentCardID.c_str());
  850. }
  851. }
  852. // Acquire Channels for current IOSelection
  853. if (!cardEntry->AcquireInputSelection(want_props.ioSelect,
  854. want_props.deviceID,
  855. ajaSource->GetName())) {
  856. blog(LOG_ERROR,
  857. "aja_source_update: Could not acquire IOSelection %s",
  858. aja::IOSelectionToString(want_props.ioSelect).c_str());
  859. return;
  860. }
  861. // Read SDI video payload IDs (VPID) used for helping to determine the wire format
  862. NTV2VideoFormat new_vf = want_props.videoFormat;
  863. NTV2PixelFormat new_pf = want_props.pixelFormat;
  864. if (!ajaSource->ReadWireFormats(want_props.deviceID,
  865. want_props.ioSelect, new_vf, new_pf,
  866. want_props.vpids)) {
  867. blog(LOG_ERROR, "aja_source_update: ReadWireFormats failed!");
  868. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  869. curr_props.deviceID,
  870. ajaSource->GetName());
  871. return;
  872. }
  873. // Set auto-detected formats
  874. if ((int32_t)vf_select == kAutoDetect)
  875. want_props.videoFormat = new_vf;
  876. if ((int32_t)pf_select == kAutoDetect)
  877. want_props.pixelFormat = new_pf;
  878. if (want_props.videoFormat == NTV2_FORMAT_UNKNOWN ||
  879. want_props.pixelFormat == NTV2_FBF_INVALID) {
  880. blog(LOG_ERROR,
  881. "aja_source_update: Unknown video/pixel format(s): %s / %s",
  882. NTV2VideoFormatToString(want_props.videoFormat).c_str(),
  883. NTV2FrameBufferFormatToString(want_props.pixelFormat)
  884. .c_str());
  885. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  886. curr_props.deviceID,
  887. ajaSource->GetName());
  888. return;
  889. }
  890. // Change capture format and restart capture thread
  891. if (!initialized || want_props != ajaSource->GetSourceProps()) {
  892. ajaSource->ClearConnections();
  893. NTV2XptConnections xpt_cnx;
  894. aja::Routing::ConfigureSourceRoute(
  895. want_props, NTV2_MODE_CAPTURE, card, xpt_cnx);
  896. ajaSource->CacheConnections(xpt_cnx);
  897. ajaSource->Deactivate();
  898. initialized = true;
  899. }
  900. ajaSource->SetSourceProps(want_props);
  901. aja::Routing::StartSourceAudio(want_props, card);
  902. card->SetReference(NTV2_REFERENCE_FREERUN);
  903. ajaSource->Activate(true);
  904. }
  905. static obs_properties_t *aja_source_get_properties(void *data)
  906. {
  907. obs_properties_t *props = obs_properties_create();
  908. obs_property_t *device_list = obs_properties_add_list(
  909. props, kUIPropDevice.id, obs_module_text(kUIPropDevice.text),
  910. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
  911. populate_source_device_list(device_list);
  912. obs_property_t *io_select_list = obs_properties_add_list(
  913. props, kUIPropInput.id, obs_module_text(kUIPropInput.text),
  914. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  915. obs_property_t *vid_fmt_list = obs_properties_add_list(
  916. props, kUIPropVideoFormatSelect.id,
  917. obs_module_text(kUIPropVideoFormatSelect.text),
  918. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  919. obs_properties_add_list(props, kUIPropPixelFormatSelect.id,
  920. obs_module_text(kUIPropPixelFormatSelect.text),
  921. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  922. obs_properties_add_list(props, kUIPropSDITransport.id,
  923. obs_module_text(kUIPropSDITransport.text),
  924. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  925. obs_properties_add_list(props, kUIPropSDITransport4K.id,
  926. obs_module_text(kUIPropSDITransport4K.text),
  927. OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT);
  928. obs_properties_add_bool(
  929. props, kUIPropDeactivateWhenNotShowing.id,
  930. obs_module_text(kUIPropDeactivateWhenNotShowing.text));
  931. obs_property_set_modified_callback(vid_fmt_list,
  932. aja_video_format_changed);
  933. obs_property_set_modified_callback2(device_list,
  934. aja_source_device_changed, data);
  935. obs_property_set_modified_callback2(io_select_list,
  936. aja_io_selection_changed, data);
  937. return props;
  938. }
  939. void aja_source_get_defaults(obs_data_t *settings)
  940. {
  941. obs_data_set_default_int(settings, kUIPropInput.id,
  942. static_cast<long long>(IOSelection::Invalid));
  943. obs_data_set_default_int(settings, kUIPropVideoFormatSelect.id,
  944. static_cast<long long>(kAutoDetect));
  945. obs_data_set_default_int(settings, kUIPropPixelFormatSelect.id,
  946. static_cast<long long>(kAutoDetect));
  947. obs_data_set_default_int(settings, kUIPropSDITransport.id,
  948. static_cast<long long>(kAutoDetect));
  949. obs_data_set_default_int(
  950. settings, kUIPropSDITransport4K.id,
  951. static_cast<long long>(SDITransport4K::TwoSampleInterleave));
  952. obs_data_set_default_bool(settings, kUIPropDeactivateWhenNotShowing.id,
  953. false);
  954. }
  955. void aja_source_save(void *data, obs_data_t *settings)
  956. {
  957. AJASource *ajaSource = (AJASource *)data;
  958. if (!ajaSource) {
  959. blog(LOG_ERROR,
  960. "aja_source_save: AJA Source instance is null!");
  961. return;
  962. }
  963. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  964. if (!cardID || !cardID[0])
  965. return;
  966. auto &cardManager = aja::CardManager::Instance();
  967. auto cardEntry = cardManager.GetCardEntry(cardID);
  968. if (!cardEntry) {
  969. blog(LOG_DEBUG, "aja_source_save: Card Entry not found for %s",
  970. cardID);
  971. return;
  972. }
  973. auto oldName = ajaSource->GetName();
  974. auto newName = obs_source_get_name(ajaSource->GetOBSSource());
  975. if (oldName != newName &&
  976. cardEntry->UpdateChannelOwnerName(oldName, newName)) {
  977. ajaSource->SetName(newName);
  978. blog(LOG_DEBUG, "aja_source_save: Renamed \"%s\" to \"%s\"",
  979. oldName.c_str(), newName);
  980. }
  981. }
  982. struct obs_source_info create_aja_source_info()
  983. {
  984. struct obs_source_info aja_source_info = {};
  985. aja_source_info.id = kUIPropCaptureModule.id;
  986. aja_source_info.type = OBS_SOURCE_TYPE_INPUT;
  987. aja_source_info.output_flags = OBS_SOURCE_ASYNC_VIDEO |
  988. OBS_SOURCE_AUDIO |
  989. OBS_SOURCE_DO_NOT_DUPLICATE;
  990. aja_source_info.get_name = aja_source_get_name;
  991. aja_source_info.create = aja_source_create;
  992. aja_source_info.destroy = aja_source_destroy;
  993. aja_source_info.update = aja_source_update;
  994. aja_source_info.show = aja_source_show;
  995. aja_source_info.hide = aja_source_hide;
  996. aja_source_info.activate = aja_source_activate;
  997. aja_source_info.deactivate = aja_source_deactivate;
  998. aja_source_info.get_properties = aja_source_get_properties;
  999. aja_source_info.get_defaults = aja_source_get_defaults;
  1000. aja_source_info.save = aja_source_save;
  1001. aja_source_info.icon_type = OBS_ICON_TYPE_CAMERA;
  1002. return aja_source_info;
  1003. }