aja-source.cpp 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  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. ajaSource->ResetVideoBuffer(sourceProps.videoFormat,
  165. sourceProps.pixelFormat);
  166. auto inputSource = sourceProps.InitialInputSource();
  167. auto channel = sourceProps.Channel();
  168. auto audioSystem = sourceProps.AudioSystem();
  169. // Current "on-air" frame on the card. The capture thread "Ping-pongs" between
  170. // two frames, starting at an index corresponding to the framestore channel.
  171. // For example:
  172. // Channel 1 (index 0) = frames 0/1
  173. // Channel 2 (index 1) = frames 2/3
  174. // Channel 3 (index 2) = frames 4/5
  175. // Channel 4 (index 3) = frames 6/7
  176. // etc...
  177. ULWord currentCardFrame = (uint32_t)channel * 2;
  178. card->WaitForInputFieldID(NTV2_FIELD0, channel);
  179. currentCardFrame ^= 1;
  180. card->SetInputFrame(channel, currentCardFrame);
  181. AudioOffsets offsets;
  182. ResetAudioBufferOffsets(card, audioSystem, offsets);
  183. obs_data_t *settings = obs_source_get_settings(ajaSource->mSource);
  184. while (ajaSource->IsCapturing()) {
  185. if (card->GetModelName() == "(Not Found)") {
  186. os_sleep_ms(250);
  187. obs_source_update(ajaSource->mSource, settings);
  188. break;
  189. }
  190. auto videoFormat = sourceProps.videoFormat;
  191. auto pixelFormat = sourceProps.pixelFormat;
  192. auto ioSelection = sourceProps.ioSelect;
  193. bool audioOverrun = false;
  194. card->WaitForInputFieldID(NTV2_FIELD0, channel);
  195. currentCardFrame ^= 1;
  196. // Card format detection -- restarts capture thread via aja_source_update callback
  197. auto newVideoFormat = card->GetInputVideoFormat(
  198. inputSource, aja::Is3GLevelB(card, channel));
  199. if (newVideoFormat == NTV2_FORMAT_UNKNOWN) {
  200. blog(LOG_DEBUG,
  201. "AJASource::CaptureThread: Video format unknown!");
  202. ajaSource->GenerateTestPattern(videoFormat, pixelFormat,
  203. NTV2_TestPatt_Black);
  204. os_sleep_ms(250);
  205. continue;
  206. }
  207. newVideoFormat = aja::HandleSpecialCaseFormats(
  208. ioSelection, newVideoFormat, sourceProps.deviceID);
  209. if (sourceProps.autoDetect && (videoFormat != newVideoFormat)) {
  210. blog(LOG_INFO,
  211. "AJASource::CaptureThread: New Video Format detected! Triggering 'aja_source_update' callback and returning...");
  212. blog(LOG_INFO,
  213. "AJASource::CaptureThread: Current Video Format: %s, | Want Video Format: %s",
  214. NTV2VideoFormatToString(videoFormat, true).c_str(),
  215. NTV2VideoFormatToString(newVideoFormat, true)
  216. .c_str());
  217. os_sleep_ms(250);
  218. obs_source_update(ajaSource->mSource, settings);
  219. break;
  220. }
  221. card->ReadAudioLastIn(offsets.currentAddress, audioSystem);
  222. offsets.currentAddress &= ~0x3; // Force DWORD alignment
  223. offsets.currentAddress += offsets.readOffset;
  224. if (offsets.currentAddress < offsets.lastAddress) {
  225. offsets.bytesRead =
  226. offsets.wrapAddress - offsets.lastAddress;
  227. if (offsets.bytesRead >
  228. ajaSource->mAudioBuffer.GetByteCount()) {
  229. blog(LOG_DEBUG,
  230. "AJASource::CaptureThread: Audio overrun (1)! Buffer Size: %d, Bytes Captured: %d",
  231. ajaSource->mAudioBuffer.GetByteCount(),
  232. offsets.bytesRead);
  233. ResetAudioBufferOffsets(card, audioSystem,
  234. offsets);
  235. audioOverrun = true;
  236. }
  237. if (!audioOverrun) {
  238. card->DMAReadAudio(audioSystem,
  239. ajaSource->mAudioBuffer,
  240. offsets.lastAddress,
  241. offsets.bytesRead);
  242. card->DMAReadAudio(
  243. audioSystem,
  244. reinterpret_cast<ULWord *>(
  245. ajaSource->mAudioBuffer
  246. .GetHostAddress(
  247. offsets.bytesRead)),
  248. offsets.readOffset,
  249. offsets.currentAddress -
  250. offsets.readOffset);
  251. offsets.bytesRead += offsets.currentAddress -
  252. offsets.readOffset;
  253. }
  254. if (offsets.bytesRead >
  255. ajaSource->mAudioBuffer.GetByteCount()) {
  256. blog(LOG_DEBUG,
  257. "AJASource::CaptureThread: Audio overrun (2)! Buffer Size: %d, Bytes Captured: %d",
  258. ajaSource->mAudioBuffer.GetByteCount(),
  259. offsets.bytesRead);
  260. ResetAudioBufferOffsets(card, audioSystem,
  261. offsets);
  262. audioOverrun = true;
  263. }
  264. } else {
  265. offsets.bytesRead =
  266. offsets.currentAddress - offsets.lastAddress;
  267. if (offsets.bytesRead >
  268. ajaSource->mAudioBuffer.GetByteCount()) {
  269. blog(LOG_DEBUG,
  270. "AJASource::CaptureThread: Audio overrun (3)! Buffer Size: %d, Bytes Captured: %d",
  271. ajaSource->mAudioBuffer.GetByteCount(),
  272. offsets.bytesRead);
  273. ResetAudioBufferOffsets(card, audioSystem,
  274. offsets);
  275. audioOverrun = true;
  276. }
  277. if (!audioOverrun) {
  278. card->DMAReadAudio(audioSystem,
  279. ajaSource->mAudioBuffer,
  280. offsets.lastAddress,
  281. offsets.bytesRead);
  282. }
  283. }
  284. if (!audioOverrun) {
  285. offsets.lastAddress = offsets.currentAddress;
  286. obs_source_audio audioPacket;
  287. audioPacket.samples_per_sec = 48000;
  288. audioPacket.format = AUDIO_FORMAT_32BIT;
  289. audioPacket.speakers = SPEAKERS_7POINT1;
  290. audioPacket.frames = offsets.bytesRead / 32;
  291. audioPacket.timestamp = os_gettime_ns();
  292. audioPacket.data[0] = (uint8_t *)ajaSource->mAudioBuffer
  293. .GetHostPointer();
  294. obs_source_output_audio(ajaSource->mSource,
  295. &audioPacket);
  296. }
  297. if (ajaSource->mVideoBuffer.GetByteCount() == 0) {
  298. blog(LOG_DEBUG,
  299. "AJASource::CaptureThread: 0 bytes in video buffer! Something went wrong!");
  300. continue;
  301. }
  302. card->DMAReadFrame(currentCardFrame, ajaSource->mVideoBuffer,
  303. ajaSource->mVideoBuffer.GetByteCount());
  304. auto actualVideoFormat = videoFormat;
  305. if (aja::Is3GLevelB(card, channel))
  306. actualVideoFormat = aja::GetLevelAFormatForLevelBFormat(
  307. videoFormat);
  308. NTV2FormatDesc fd(actualVideoFormat, pixelFormat);
  309. struct obs_source_frame2 obsFrame;
  310. obsFrame.flip = false;
  311. obsFrame.timestamp = os_gettime_ns();
  312. obsFrame.width = fd.GetRasterWidth();
  313. obsFrame.height = fd.GetRasterHeight();
  314. obsFrame.format = aja::AJAPixelFormatToOBSVideoFormat(
  315. sourceProps.pixelFormat);
  316. obsFrame.data[0] = reinterpret_cast<uint8_t *>(
  317. (ULWord *)ajaSource->mVideoBuffer.GetHostPointer());
  318. obsFrame.linesize[0] = fd.GetBytesPerRow();
  319. video_format_get_parameters(VIDEO_CS_DEFAULT, VIDEO_RANGE_FULL,
  320. obsFrame.color_matrix,
  321. obsFrame.color_range_min,
  322. obsFrame.color_range_max);
  323. obs_source_output_video2(ajaSource->mSource, &obsFrame);
  324. card->SetInputFrame(channel, currentCardFrame);
  325. }
  326. blog(LOG_INFO, "AJASource::Capturethread: Thread loop stopped");
  327. ajaSource->GenerateTestPattern(sourceProps.videoFormat,
  328. sourceProps.pixelFormat,
  329. NTV2_TestPatt_Black);
  330. obs_data_release(settings);
  331. }
  332. void AJASource::Deactivate()
  333. {
  334. SetCapturing(false);
  335. if (mCaptureThread) {
  336. if (mCaptureThread->Active()) {
  337. mCaptureThread->Stop();
  338. blog(LOG_INFO, "AJASource::CaptureThread: Stopped!");
  339. }
  340. delete mCaptureThread;
  341. mCaptureThread = nullptr;
  342. blog(LOG_INFO, "AJASource::CaptureThread: Destroyed!");
  343. }
  344. }
  345. void AJASource::Activate(bool enable)
  346. {
  347. if (mCaptureThread == nullptr) {
  348. mCaptureThread = new AJAThread();
  349. mCaptureThread->Attach(AJASource::CaptureThread, this);
  350. mCaptureThread->SetPriority(AJA_ThreadPriority_High);
  351. blog(LOG_INFO, "AJASource::CaptureThread: Created!");
  352. }
  353. if (enable) {
  354. SetCapturing(true);
  355. if (!mCaptureThread->Active()) {
  356. mCaptureThread->Start();
  357. blog(LOG_INFO, "AJASource::CaptureThread: Started!");
  358. }
  359. }
  360. }
  361. bool AJASource::IsCapturing() const
  362. {
  363. return mIsCapturing;
  364. }
  365. void AJASource::SetCapturing(bool capturing)
  366. {
  367. std::lock_guard<std::mutex> lock(mMutex);
  368. mIsCapturing = capturing;
  369. }
  370. //
  371. // CardEntry/Device stuff
  372. //
  373. std::string AJASource::CardID() const
  374. {
  375. return mCardID;
  376. }
  377. void AJASource::SetCardID(const std::string &cardID)
  378. {
  379. mCardID = cardID;
  380. }
  381. uint32_t AJASource::DeviceIndex() const
  382. {
  383. return static_cast<uint32_t>(mDeviceIndex);
  384. }
  385. void AJASource::SetDeviceIndex(uint32_t index)
  386. {
  387. mDeviceIndex = static_cast<UWord>(index);
  388. }
  389. //
  390. // AJASource Properties stuff
  391. //
  392. void AJASource::SetSourceProps(const SourceProps &props)
  393. {
  394. mSourceProps = props;
  395. }
  396. SourceProps AJASource::GetSourceProps() const
  397. {
  398. return mSourceProps;
  399. }
  400. bool AJASource::ReadChannelVPIDs(NTV2Channel channel, VPIDData &vpids)
  401. {
  402. ULWord vpid_a = 0;
  403. ULWord vpid_b = 0;
  404. bool read_ok = mCard->ReadSDIInVPID(channel, vpid_a, vpid_b);
  405. vpids.SetA(vpid_a);
  406. vpids.SetB(vpid_b);
  407. vpids.Parse();
  408. return read_ok;
  409. }
  410. bool AJASource::ReadWireFormats(NTV2DeviceID device_id, IOSelection io_select,
  411. NTV2VideoFormat &vf, NTV2PixelFormat &pf,
  412. VPIDDataList &vpids)
  413. {
  414. NTV2InputSourceSet input_srcs;
  415. aja::IOSelectionToInputSources(io_select, input_srcs);
  416. if (input_srcs.empty()) {
  417. blog(LOG_INFO,
  418. "AJASource::ReadWireFormats: No NTV2InputSources found for IOSelection %s",
  419. aja::IOSelectionToString(io_select).c_str());
  420. return false;
  421. }
  422. NTV2InputSource initial_src = *input_srcs.begin();
  423. for (auto &&src : input_srcs) {
  424. auto channel = NTV2InputSourceToChannel(src);
  425. mCard->EnableChannel(channel);
  426. if (NTV2_INPUT_SOURCE_IS_SDI(src)) {
  427. if (NTV2DeviceHasBiDirectionalSDI(device_id)) {
  428. mCard->SetSDITransmitEnable(channel, false);
  429. }
  430. mCard->WaitForInputVerticalInterrupt(channel);
  431. VPIDData vpid_data;
  432. if (ReadChannelVPIDs(channel, vpid_data))
  433. vpids.push_back(vpid_data);
  434. } else if (NTV2_INPUT_SOURCE_IS_HDMI(src)) {
  435. mCard->WaitForInputVerticalInterrupt(channel);
  436. ULWord hdmi_version =
  437. NTV2DeviceGetHDMIVersion(device_id);
  438. // HDMIv1 handles its own RGB->YCbCr color space conversion
  439. if (hdmi_version == 1) {
  440. pf = kDefaultAJAPixelFormat;
  441. } else {
  442. NTV2LHIHDMIColorSpace hdmiInputColor;
  443. mCard->GetHDMIInputColor(hdmiInputColor,
  444. channel);
  445. if (hdmiInputColor ==
  446. NTV2_LHIHDMIColorSpaceYCbCr) {
  447. pf = kDefaultAJAPixelFormat;
  448. } else if (hdmiInputColor ==
  449. NTV2_LHIHDMIColorSpaceRGB) {
  450. pf = NTV2_FBF_24BIT_BGR;
  451. }
  452. }
  453. }
  454. }
  455. NTV2Channel initial_channel = NTV2InputSourceToChannel(initial_src);
  456. mCard->WaitForInputVerticalInterrupt(initial_channel);
  457. vf = mCard->GetInputVideoFormat(
  458. initial_src, aja::Is3GLevelB(mCard, initial_channel));
  459. if (NTV2_INPUT_SOURCE_IS_SDI(initial_src)) {
  460. if (vpids.size() > 0) {
  461. auto vpid = *vpids.begin();
  462. if (vpid.Sampling() == VPIDSampling_YUV_422) {
  463. pf = NTV2_FBF_8BIT_YCBCR;
  464. blog(LOG_INFO,
  465. "AJASource::ReadWireFormats - Detected pixel format %s",
  466. NTV2FrameBufferFormatToString(pf, true)
  467. .c_str());
  468. } else if (vpid.Sampling() == VPIDSampling_GBR_444) {
  469. pf = NTV2_FBF_24BIT_BGR;
  470. blog(LOG_INFO,
  471. "AJASource::ReadWireFormats - Detected pixel format %s",
  472. NTV2FrameBufferFormatToString(pf, true)
  473. .c_str());
  474. }
  475. }
  476. }
  477. vf = aja::HandleSpecialCaseFormats(io_select, vf, device_id);
  478. blog(LOG_INFO, "AJASource::ReadWireFormats - Detected video format %s",
  479. NTV2VideoFormatToString(vf).c_str());
  480. return true;
  481. }
  482. void AJASource::ResetVideoBuffer(NTV2VideoFormat vf, NTV2PixelFormat pf)
  483. {
  484. if (vf != NTV2_FORMAT_UNKNOWN) {
  485. auto videoBufferSize = GetVideoWriteSize(vf, pf);
  486. if (mVideoBuffer)
  487. mVideoBuffer.Deallocate();
  488. mVideoBuffer.Allocate(videoBufferSize, true);
  489. blog(LOG_INFO,
  490. "AJASource::ResetVideoBuffer: Video Format: %s | Pixel Format: %s | Buffer Size: %d",
  491. NTV2VideoFormatToString(vf, false).c_str(),
  492. NTV2FrameBufferFormatToString(pf, true).c_str(),
  493. videoBufferSize);
  494. }
  495. }
  496. void AJASource::ResetAudioBuffer(size_t size)
  497. {
  498. if (mAudioBuffer)
  499. mAudioBuffer.Deallocate();
  500. mAudioBuffer.Allocate(size, true);
  501. }
  502. static const char *aja_source_get_name(void *);
  503. static void *aja_source_create(obs_data_t *, obs_source_t *);
  504. static void aja_source_destroy(void *);
  505. static void aja_source_activate(void *);
  506. static void aja_source_deactivate(void *);
  507. static void aja_source_update(void *, obs_data_t *);
  508. const char *aja_source_get_name(void *unused)
  509. {
  510. UNUSED_PARAMETER(unused);
  511. return obs_module_text(kUIPropCaptureModule.text);
  512. }
  513. bool aja_source_device_changed(void *data, obs_properties_t *props,
  514. obs_property_t *list, obs_data_t *settings)
  515. {
  516. UNUSED_PARAMETER(list);
  517. blog(LOG_DEBUG, "AJA Source Device Changed");
  518. auto *ajaSource = (AJASource *)data;
  519. if (!ajaSource) {
  520. blog(LOG_DEBUG,
  521. "aja_source_device_changed: AJA Source instance is null!");
  522. return false;
  523. }
  524. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  525. if (!cardID || !cardID[0])
  526. return false;
  527. auto &cardManager = aja::CardManager::Instance();
  528. auto cardEntry = cardManager.GetCardEntry(cardID);
  529. if (!cardEntry) {
  530. blog(LOG_DEBUG,
  531. "aja_source_device_changed: Card Entry not found for %s",
  532. cardID);
  533. return false;
  534. }
  535. blog(LOG_DEBUG, "Found CardEntry for %s", cardID);
  536. CNTV2Card *card = cardEntry->GetCard();
  537. if (!card) {
  538. blog(LOG_DEBUG,
  539. "aja_source_device_changed: Card instance is null!");
  540. return false;
  541. }
  542. const NTV2DeviceID deviceID = card->GetDeviceID();
  543. /* If Channel 1 is actively in use, filter the video format list to only
  544. * show video formats within the same framerate family. If Channel 1 is
  545. * not active we just go ahead and try to set all framestores to the same video format.
  546. * This is because Channel 1's clock rate will govern the card's Free Run clock.
  547. */
  548. NTV2VideoFormat videoFormatChannel1 = NTV2_FORMAT_UNKNOWN;
  549. if (!cardEntry->ChannelReady(NTV2_CHANNEL1, ajaSource->GetName())) {
  550. card->GetVideoFormat(videoFormatChannel1, NTV2_CHANNEL1);
  551. }
  552. obs_property_t *devices_list =
  553. obs_properties_get(props, kUIPropDevice.id);
  554. obs_property_t *io_select_list =
  555. obs_properties_get(props, kUIPropInput.id);
  556. obs_property_t *vid_fmt_list =
  557. obs_properties_get(props, kUIPropVideoFormatSelect.id);
  558. obs_property_t *pix_fmt_list =
  559. obs_properties_get(props, kUIPropPixelFormatSelect.id);
  560. obs_property_t *sdi_trx_list =
  561. obs_properties_get(props, kUIPropSDITransport.id);
  562. obs_property_t *sdi_4k_list =
  563. obs_properties_get(props, kUIPropSDITransport4K.id);
  564. obs_property_list_clear(vid_fmt_list);
  565. obs_property_list_add_int(vid_fmt_list, obs_module_text("Auto"),
  566. kAutoDetect);
  567. populate_video_format_list(deviceID, vid_fmt_list, videoFormatChannel1);
  568. obs_property_list_clear(pix_fmt_list);
  569. obs_property_list_add_int(pix_fmt_list, obs_module_text("Auto"),
  570. kAutoDetect);
  571. populate_pixel_format_list(deviceID, pix_fmt_list);
  572. IOSelection io_select = static_cast<IOSelection>(
  573. obs_data_get_int(settings, kUIPropInput.id));
  574. obs_property_list_clear(sdi_trx_list);
  575. obs_property_list_add_int(sdi_trx_list, obs_module_text("Auto"),
  576. kAutoDetect);
  577. populate_sdi_transport_list(sdi_trx_list, io_select);
  578. obs_property_list_clear(sdi_4k_list);
  579. populate_sdi_4k_transport_list(sdi_4k_list);
  580. populate_io_selection_input_list(cardID, ajaSource->GetName(), deviceID,
  581. io_select_list);
  582. auto curr_vf = static_cast<NTV2VideoFormat>(
  583. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  584. bool have_cards = cardManager.NumCardEntries() > 0;
  585. obs_property_set_visible(devices_list, have_cards);
  586. obs_property_set_visible(io_select_list, have_cards);
  587. obs_property_set_visible(vid_fmt_list, have_cards);
  588. obs_property_set_visible(pix_fmt_list, have_cards);
  589. obs_property_set_visible(
  590. sdi_4k_list, have_cards && NTV2_IS_4K_VIDEO_FORMAT(curr_vf));
  591. return true;
  592. }
  593. bool aja_io_selection_changed(void *data, obs_properties_t *props,
  594. obs_property_t *list, obs_data_t *settings)
  595. {
  596. UNUSED_PARAMETER(list);
  597. AJASource *ajaSource = (AJASource *)data;
  598. if (!ajaSource) {
  599. blog(LOG_DEBUG,
  600. "aja_io_selection_changed: AJA Source instance is null!");
  601. return false;
  602. }
  603. const char *cardID = obs_data_get_string(settings, kUIPropDevice.id);
  604. if (!cardID || !cardID[0])
  605. return false;
  606. auto &cardManager = aja::CardManager::Instance();
  607. auto cardEntry = cardManager.GetCardEntry(cardID);
  608. if (!cardEntry) {
  609. blog(LOG_DEBUG,
  610. "aja_io_selection_changed: Card Entry not found for %s",
  611. cardID);
  612. return false;
  613. }
  614. obs_property_t *io_select_list =
  615. obs_properties_get(props, kUIPropInput.id);
  616. filter_io_selection_input_list(cardID, ajaSource->GetName(),
  617. io_select_list);
  618. return true;
  619. }
  620. bool aja_sdi_mode_list_changed(obs_properties_t *props, obs_property_t *list,
  621. obs_data_t *settings)
  622. {
  623. UNUSED_PARAMETER(props);
  624. UNUSED_PARAMETER(list);
  625. UNUSED_PARAMETER(settings);
  626. return true;
  627. }
  628. void *aja_source_create(obs_data_t *settings, obs_source_t *source)
  629. {
  630. blog(LOG_DEBUG, "AJA Source Create");
  631. auto ajaSource = new AJASource(source);
  632. ajaSource->SetName(obs_source_get_name(source));
  633. obs_source_set_async_decoupled(source, true);
  634. ajaSource->SetOBSSource(source);
  635. ajaSource->ResetAudioBuffer(NTV2_AUDIOSIZE_MAX);
  636. ajaSource->Activate(false);
  637. obs_source_update(source, settings);
  638. return ajaSource;
  639. }
  640. void aja_source_destroy(void *data)
  641. {
  642. blog(LOG_DEBUG, "AJA Source Destroy");
  643. auto ajaSource = (AJASource *)data;
  644. if (!ajaSource) {
  645. blog(LOG_ERROR, "aja_source_destroy: Plugin instance is null!");
  646. return;
  647. }
  648. ajaSource->Deactivate();
  649. NTV2DeviceID deviceID = DEVICE_ID_NOTFOUND;
  650. CNTV2Card *card = ajaSource->GetCard();
  651. if (card) {
  652. deviceID = card->GetDeviceID();
  653. aja::Routing::StopSourceAudio(ajaSource->GetSourceProps(),
  654. card);
  655. }
  656. ajaSource->mVideoBuffer.Deallocate();
  657. ajaSource->mAudioBuffer.Deallocate();
  658. ajaSource->mVideoBuffer = NULL;
  659. ajaSource->mAudioBuffer = NULL;
  660. auto &cardManager = aja::CardManager::Instance();
  661. const auto &cardID = ajaSource->CardID();
  662. auto cardEntry = cardManager.GetCardEntry(cardID);
  663. if (!cardEntry) {
  664. blog(LOG_DEBUG,
  665. "aja_source_destroy: Card Entry not found for %s",
  666. cardID.c_str());
  667. return;
  668. }
  669. auto ioSelect = ajaSource->GetSourceProps().ioSelect;
  670. if (!cardEntry->ReleaseInputSelection(ioSelect, deviceID,
  671. ajaSource->GetName())) {
  672. blog(LOG_WARNING,
  673. "aja_source_destroy: Error releasing Input Selection!");
  674. }
  675. delete ajaSource;
  676. ajaSource = nullptr;
  677. }
  678. static void aja_source_show(void *data)
  679. {
  680. auto ajaSource = (AJASource *)data;
  681. if (!ajaSource) {
  682. blog(LOG_ERROR,
  683. "aja_source_show: AJA Source instance is null!");
  684. return;
  685. }
  686. bool deactivateWhileNotShowing =
  687. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  688. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  689. blog(LOG_DEBUG,
  690. "aja_source_show: deactivateWhileNotShowing = %s, showing = %s",
  691. deactivateWhileNotShowing ? "true" : "false",
  692. showing ? "true" : "false");
  693. if (deactivateWhileNotShowing && showing && !ajaSource->IsCapturing()) {
  694. ajaSource->Activate(true);
  695. blog(LOG_DEBUG, "aja_source_show: activated capture thread!");
  696. }
  697. }
  698. static void aja_source_hide(void *data)
  699. {
  700. auto ajaSource = (AJASource *)data;
  701. if (!ajaSource)
  702. return;
  703. bool deactivateWhileNotShowing =
  704. ajaSource->GetSourceProps().deactivateWhileNotShowing;
  705. bool showing = obs_source_showing(ajaSource->GetOBSSource());
  706. blog(LOG_DEBUG,
  707. "aja_source_hide: deactivateWhileNotShowing = %s, showing = %s",
  708. deactivateWhileNotShowing ? "true" : "false",
  709. showing ? "true" : "false");
  710. if (deactivateWhileNotShowing && !showing && ajaSource->IsCapturing()) {
  711. ajaSource->Deactivate();
  712. blog(LOG_DEBUG, "aja_source_hide: deactivated capture thread!");
  713. }
  714. }
  715. static void aja_source_activate(void *data)
  716. {
  717. UNUSED_PARAMETER(data);
  718. }
  719. static void aja_source_deactivate(void *data)
  720. {
  721. UNUSED_PARAMETER(data);
  722. }
  723. static void aja_source_update(void *data, obs_data_t *settings)
  724. {
  725. static bool initialized = false;
  726. auto ajaSource = (AJASource *)data;
  727. if (!ajaSource) {
  728. blog(LOG_WARNING,
  729. "aja_source_update: Plugin instance is null!");
  730. return;
  731. }
  732. auto io_select = static_cast<IOSelection>(
  733. obs_data_get_int(settings, kUIPropInput.id));
  734. auto vf_select = static_cast<NTV2VideoFormat>(
  735. obs_data_get_int(settings, kUIPropVideoFormatSelect.id));
  736. auto pf_select = static_cast<NTV2PixelFormat>(
  737. obs_data_get_int(settings, kUIPropPixelFormatSelect.id));
  738. auto sdi_trx_select = static_cast<SDITransport>(
  739. obs_data_get_int(settings, kUIPropSDITransport.id));
  740. auto sdi_t4k_select = static_cast<SDITransport4K>(
  741. obs_data_get_int(settings, kUIPropSDITransport4K.id));
  742. bool deactivateWhileNotShowing =
  743. obs_data_get_bool(settings, kUIPropDeactivateWhenNotShowing.id);
  744. const std::string &wantCardID =
  745. obs_data_get_string(settings, kUIPropDevice.id);
  746. const std::string &currentCardID = ajaSource->CardID();
  747. if (wantCardID != currentCardID) {
  748. initialized = false;
  749. ajaSource->Deactivate();
  750. }
  751. auto &cardManager = aja::CardManager::Instance();
  752. cardManager.EnumerateCards();
  753. auto cardEntry = cardManager.GetCardEntry(wantCardID);
  754. if (!cardEntry) {
  755. blog(LOG_DEBUG,
  756. "aja_source_update: Card Entry not found for %s",
  757. wantCardID.c_str());
  758. return;
  759. }
  760. CNTV2Card *card = cardEntry->GetCard();
  761. if (!card || !card->IsOpen()) {
  762. blog(LOG_ERROR, "aja_source_update: AJA device %s not open!",
  763. wantCardID.c_str());
  764. return;
  765. }
  766. if (card->GetModelName() == "(Not Found)") {
  767. blog(LOG_ERROR,
  768. "aja_source_update: AJA device %s disconnected?",
  769. wantCardID.c_str());
  770. return;
  771. }
  772. ajaSource->SetCard(cardEntry->GetCard());
  773. SourceProps curr_props = ajaSource->GetSourceProps();
  774. // Release Channels from previous card if card ID changes
  775. if (wantCardID != currentCardID) {
  776. auto prevCardEntry = cardManager.GetCardEntry(currentCardID);
  777. if (prevCardEntry) {
  778. const std::string &ioSelectStr =
  779. aja::IOSelectionToString(curr_props.ioSelect);
  780. if (!prevCardEntry->ReleaseInputSelection(
  781. curr_props.ioSelect, curr_props.deviceID,
  782. ajaSource->GetName())) {
  783. blog(LOG_WARNING,
  784. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  785. ioSelectStr.c_str(),
  786. currentCardID.c_str());
  787. } else {
  788. blog(LOG_INFO,
  789. "aja_source_update: Released IOSelection %s for card ID %s",
  790. ioSelectStr.c_str(),
  791. currentCardID.c_str());
  792. ajaSource->SetCardID(wantCardID);
  793. io_select = IOSelection::Invalid;
  794. }
  795. }
  796. }
  797. if (io_select == IOSelection::Invalid) {
  798. blog(LOG_DEBUG, "aja_source_update: Invalid IOSelection");
  799. return;
  800. }
  801. SourceProps want_props;
  802. want_props.deviceID = card->GetDeviceID();
  803. want_props.ioSelect = io_select;
  804. want_props.videoFormat =
  805. ((int32_t)vf_select == kAutoDetect)
  806. ? NTV2_FORMAT_UNKNOWN
  807. : static_cast<NTV2VideoFormat>(vf_select);
  808. want_props.pixelFormat =
  809. ((int32_t)pf_select == kAutoDetect)
  810. ? NTV2_FBF_INVALID
  811. : static_cast<NTV2PixelFormat>(pf_select);
  812. want_props.sdiTransport =
  813. ((int32_t)sdi_trx_select == kAutoDetect)
  814. ? SDITransport::Unknown
  815. : static_cast<SDITransport>(sdi_trx_select);
  816. want_props.sdi4kTransport = sdi_t4k_select;
  817. want_props.vpids.clear();
  818. want_props.deactivateWhileNotShowing = deactivateWhileNotShowing;
  819. want_props.autoDetect = ((int32_t)vf_select == kAutoDetect ||
  820. (int32_t)pf_select == kAutoDetect);
  821. ajaSource->SetCardID(wantCardID);
  822. ajaSource->SetDeviceIndex((UWord)cardEntry->GetCardIndex());
  823. if (NTV2_IS_4K_VIDEO_FORMAT(want_props.videoFormat) &&
  824. want_props.sdi4kTransport == SDITransport4K::Squares) {
  825. if (want_props.ioSelect == IOSelection::SDI1_2) {
  826. want_props.ioSelect = IOSelection::SDI1_2_Squares;
  827. } else if (want_props.ioSelect == IOSelection::SDI3_4) {
  828. want_props.ioSelect = IOSelection::SDI3_4_Squares;
  829. }
  830. }
  831. // Release Channels if IOSelection changes
  832. if (want_props.ioSelect != curr_props.ioSelect) {
  833. const std::string &ioSelectStr =
  834. aja::IOSelectionToString(curr_props.ioSelect);
  835. if (!cardEntry->ReleaseInputSelection(curr_props.ioSelect,
  836. curr_props.deviceID,
  837. ajaSource->GetName())) {
  838. blog(LOG_WARNING,
  839. "aja_source_update: Error releasing IOSelection %s for card ID %s",
  840. ioSelectStr.c_str(), currentCardID.c_str());
  841. } else {
  842. blog(LOG_INFO,
  843. "aja_source_update: Released IOSelection %s for card ID %s",
  844. ioSelectStr.c_str(), currentCardID.c_str());
  845. }
  846. }
  847. // Acquire Channels for current IOSelection
  848. if (!cardEntry->AcquireInputSelection(want_props.ioSelect,
  849. want_props.deviceID,
  850. ajaSource->GetName())) {
  851. blog(LOG_ERROR,
  852. "aja_source_update: Could not acquire IOSelection %s",
  853. aja::IOSelectionToString(want_props.ioSelect).c_str());
  854. return;
  855. }
  856. // Read SDI video payload IDs (VPID) used for helping to determine the wire format
  857. NTV2VideoFormat new_vf = want_props.videoFormat;
  858. NTV2PixelFormat new_pf = want_props.pixelFormat;
  859. if (!ajaSource->ReadWireFormats(want_props.deviceID,
  860. want_props.ioSelect, new_vf, new_pf,
  861. want_props.vpids)) {
  862. blog(LOG_ERROR, "aja_source_update: ReadWireFormats failed!");
  863. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  864. curr_props.deviceID,
  865. ajaSource->GetName());
  866. return;
  867. }
  868. // Set auto-detected formats
  869. if ((int32_t)vf_select == kAutoDetect)
  870. want_props.videoFormat = new_vf;
  871. if ((int32_t)pf_select == kAutoDetect)
  872. want_props.pixelFormat = new_pf;
  873. if (want_props.videoFormat == NTV2_FORMAT_UNKNOWN ||
  874. want_props.pixelFormat == NTV2_FBF_INVALID) {
  875. blog(LOG_ERROR,
  876. "aja_source_update: Unknown video/pixel format(s): %s / %s",
  877. NTV2VideoFormatToString(want_props.videoFormat).c_str(),
  878. NTV2FrameBufferFormatToString(want_props.pixelFormat)
  879. .c_str());
  880. cardEntry->ReleaseInputSelection(want_props.ioSelect,
  881. curr_props.deviceID,
  882. ajaSource->GetName());
  883. return;
  884. }
  885. // Change capture format and restart capture thread
  886. if (!initialized || want_props != ajaSource->GetSourceProps()) {
  887. aja::Routing::ConfigureSourceRoute(want_props,
  888. NTV2_MODE_CAPTURE, card);
  889. ajaSource->Deactivate();
  890. initialized = true;
  891. }
  892. ajaSource->SetSourceProps(want_props);
  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 || !cardID[0])
  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. }