StreamInfo.cs 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using Jellyfin.Data.Enums;
  5. using MediaBrowser.Model.Drawing;
  6. using MediaBrowser.Model.Dto;
  7. using MediaBrowser.Model.Entities;
  8. using MediaBrowser.Model.MediaInfo;
  9. using MediaBrowser.Model.Session;
  10. namespace MediaBrowser.Model.Dlna;
  11. /// <summary>
  12. /// Class holding information on a stream.
  13. /// </summary>
  14. public class StreamInfo
  15. {
  16. /// <summary>
  17. /// Initializes a new instance of the <see cref="StreamInfo"/> class.
  18. /// </summary>
  19. public StreamInfo()
  20. {
  21. AudioCodecs = [];
  22. VideoCodecs = [];
  23. SubtitleCodecs = [];
  24. StreamOptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  25. }
  26. /// <summary>
  27. /// Gets or sets the item id.
  28. /// </summary>
  29. /// <value>The item id.</value>
  30. public Guid ItemId { get; set; }
  31. /// <summary>
  32. /// Gets or sets the play method.
  33. /// </summary>
  34. /// <value>The play method.</value>
  35. public PlayMethod PlayMethod { get; set; }
  36. /// <summary>
  37. /// Gets or sets the encoding context.
  38. /// </summary>
  39. /// <value>The encoding context.</value>
  40. public EncodingContext Context { get; set; }
  41. /// <summary>
  42. /// Gets or sets the media type.
  43. /// </summary>
  44. /// <value>The media type.</value>
  45. public DlnaProfileType MediaType { get; set; }
  46. /// <summary>
  47. /// Gets or sets the container.
  48. /// </summary>
  49. /// <value>The container.</value>
  50. public string? Container { get; set; }
  51. /// <summary>
  52. /// Gets or sets the sub protocol.
  53. /// </summary>
  54. /// <value>The sub protocol.</value>
  55. public MediaStreamProtocol SubProtocol { get; set; }
  56. /// <summary>
  57. /// Gets or sets the start position ticks.
  58. /// </summary>
  59. /// <value>The start position ticks.</value>
  60. public long StartPositionTicks { get; set; }
  61. /// <summary>
  62. /// Gets or sets the segment length.
  63. /// </summary>
  64. /// <value>The segment length.</value>
  65. public int? SegmentLength { get; set; }
  66. /// <summary>
  67. /// Gets or sets the minimum segments count.
  68. /// </summary>
  69. /// <value>The minimum segments count.</value>
  70. public int? MinSegments { get; set; }
  71. /// <summary>
  72. /// Gets or sets a value indicating whether the stream can be broken on non-keyframes.
  73. /// </summary>
  74. public bool BreakOnNonKeyFrames { get; set; }
  75. /// <summary>
  76. /// Gets or sets a value indicating whether the stream requires AVC.
  77. /// </summary>
  78. public bool RequireAvc { get; set; }
  79. /// <summary>
  80. /// Gets or sets a value indicating whether the stream requires AVC.
  81. /// </summary>
  82. public bool RequireNonAnamorphic { get; set; }
  83. /// <summary>
  84. /// Gets or sets a value indicating whether timestamps should be copied.
  85. /// </summary>
  86. public bool CopyTimestamps { get; set; }
  87. /// <summary>
  88. /// Gets or sets a value indicating whether timestamps should be copied.
  89. /// </summary>
  90. public bool EnableMpegtsM2TsMode { get; set; }
  91. /// <summary>
  92. /// Gets or sets a value indicating whether the subtitle manifest is enabled.
  93. /// </summary>
  94. public bool EnableSubtitlesInManifest { get; set; }
  95. /// <summary>
  96. /// Gets or sets the audio codecs.
  97. /// </summary>
  98. /// <value>The audio codecs.</value>
  99. public IReadOnlyList<string> AudioCodecs { get; set; }
  100. /// <summary>
  101. /// Gets or sets the video codecs.
  102. /// </summary>
  103. /// <value>The video codecs.</value>
  104. public IReadOnlyList<string> VideoCodecs { get; set; }
  105. /// <summary>
  106. /// Gets or sets the audio stream index.
  107. /// </summary>
  108. /// <value>The audio stream index.</value>
  109. public int? AudioStreamIndex { get; set; }
  110. /// <summary>
  111. /// Gets or sets the video stream index.
  112. /// </summary>
  113. /// <value>The subtitle stream index.</value>
  114. public int? SubtitleStreamIndex { get; set; }
  115. /// <summary>
  116. /// Gets or sets the maximum transcoding audio channels.
  117. /// </summary>
  118. /// <value>The maximum transcoding audio channels.</value>
  119. public int? TranscodingMaxAudioChannels { get; set; }
  120. /// <summary>
  121. /// Gets or sets the global maximum audio channels.
  122. /// </summary>
  123. /// <value>The global maximum audio channels.</value>
  124. public int? GlobalMaxAudioChannels { get; set; }
  125. /// <summary>
  126. /// Gets or sets the audio bitrate.
  127. /// </summary>
  128. /// <value>The audio bitrate.</value>
  129. public int? AudioBitrate { get; set; }
  130. /// <summary>
  131. /// Gets or sets the audio sample rate.
  132. /// </summary>
  133. /// <value>The audio sample rate.</value>
  134. public int? AudioSampleRate { get; set; }
  135. /// <summary>
  136. /// Gets or sets the video bitrate.
  137. /// </summary>
  138. /// <value>The video bitrate.</value>
  139. public int? VideoBitrate { get; set; }
  140. /// <summary>
  141. /// Gets or sets the maximum output width.
  142. /// </summary>
  143. /// <value>The output width.</value>
  144. public int? MaxWidth { get; set; }
  145. /// <summary>
  146. /// Gets or sets the maximum output height.
  147. /// </summary>
  148. /// <value>The maximum output height.</value>
  149. public int? MaxHeight { get; set; }
  150. /// <summary>
  151. /// Gets or sets the maximum framerate.
  152. /// </summary>
  153. /// <value>The maximum framerate.</value>
  154. public float? MaxFramerate { get; set; }
  155. /// <summary>
  156. /// Gets or sets the device profile.
  157. /// </summary>
  158. /// <value>The device profile.</value>
  159. public required DeviceProfile DeviceProfile { get; set; }
  160. /// <summary>
  161. /// Gets or sets the device profile id.
  162. /// </summary>
  163. /// <value>The device profile id.</value>
  164. public string? DeviceProfileId { get; set; }
  165. /// <summary>
  166. /// Gets or sets the device id.
  167. /// </summary>
  168. /// <value>The device id.</value>
  169. public string? DeviceId { get; set; }
  170. /// <summary>
  171. /// Gets or sets the runtime ticks.
  172. /// </summary>
  173. /// <value>The runtime ticks.</value>
  174. public long? RunTimeTicks { get; set; }
  175. /// <summary>
  176. /// Gets or sets the transcode seek info.
  177. /// </summary>
  178. /// <value>The transcode seek info.</value>
  179. public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
  180. /// <summary>
  181. /// Gets or sets a value indicating whether content length should be estimated.
  182. /// </summary>
  183. public bool EstimateContentLength { get; set; }
  184. /// <summary>
  185. /// Gets or sets the media source info.
  186. /// </summary>
  187. /// <value>The media source info.</value>
  188. public MediaSourceInfo? MediaSource { get; set; }
  189. /// <summary>
  190. /// Gets or sets the subtitle codecs.
  191. /// </summary>
  192. /// <value>The subtitle codecs.</value>
  193. public IReadOnlyList<string> SubtitleCodecs { get; set; }
  194. /// <summary>
  195. /// Gets or sets the subtitle delivery method.
  196. /// </summary>
  197. /// <value>The subtitle delivery method.</value>
  198. public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
  199. /// <summary>
  200. /// Gets or sets the subtitle format.
  201. /// </summary>
  202. /// <value>The subtitle format.</value>
  203. public string? SubtitleFormat { get; set; }
  204. /// <summary>
  205. /// Gets or sets the play session id.
  206. /// </summary>
  207. /// <value>The play session id.</value>
  208. public string? PlaySessionId { get; set; }
  209. /// <summary>
  210. /// Gets or sets the transcode reasons.
  211. /// </summary>
  212. /// <value>The transcode reasons.</value>
  213. public TranscodeReason TranscodeReasons { get; set; }
  214. /// <summary>
  215. /// Gets the stream options.
  216. /// </summary>
  217. /// <value>The stream options.</value>
  218. public Dictionary<string, string> StreamOptions { get; private set; }
  219. /// <summary>
  220. /// Gets the media source id.
  221. /// </summary>
  222. /// <value>The media source id.</value>
  223. public string? MediaSourceId => MediaSource?.Id;
  224. /// <summary>
  225. /// Gets or sets a value indicating whether audio VBR encoding is enabled.
  226. /// </summary>
  227. public bool EnableAudioVbrEncoding { get; set; }
  228. /// <summary>
  229. /// Gets or sets a value indicating whether always burn in subtitles when transcoding.
  230. /// </summary>
  231. public bool AlwaysBurnInSubtitleWhenTranscoding { get; set; }
  232. /// <summary>
  233. /// Gets a value indicating whether the stream is direct.
  234. /// </summary>
  235. public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
  236. && PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
  237. /// <summary>
  238. /// Gets the audio stream that will be used in the output stream.
  239. /// </summary>
  240. /// <value>The audio stream.</value>
  241. public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
  242. /// <summary>
  243. /// Gets the video stream that will be used in the output stream.
  244. /// </summary>
  245. /// <value>The video stream.</value>
  246. public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
  247. /// <summary>
  248. /// Gets the audio sample rate that will be in the output stream.
  249. /// </summary>
  250. /// <value>The target audio sample rate.</value>
  251. public int? TargetAudioSampleRate
  252. {
  253. get
  254. {
  255. var stream = TargetAudioStream;
  256. return AudioSampleRate.HasValue && !IsDirectStream
  257. ? AudioSampleRate
  258. : stream?.SampleRate;
  259. }
  260. }
  261. /// <summary>
  262. /// Gets the audio bit depth that will be in the output stream.
  263. /// </summary>
  264. /// <value>The target bit depth.</value>
  265. public int? TargetAudioBitDepth
  266. {
  267. get
  268. {
  269. if (IsDirectStream)
  270. {
  271. return TargetAudioStream?.BitDepth;
  272. }
  273. var targetAudioCodecs = TargetAudioCodec;
  274. var audioCodec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
  275. if (!string.IsNullOrEmpty(audioCodec))
  276. {
  277. return GetTargetAudioBitDepth(audioCodec);
  278. }
  279. return TargetAudioStream?.BitDepth;
  280. }
  281. }
  282. /// <summary>
  283. /// Gets the video bit depth that will be in the output stream.
  284. /// </summary>
  285. /// <value>The target video bit depth.</value>
  286. public int? TargetVideoBitDepth
  287. {
  288. get
  289. {
  290. if (IsDirectStream)
  291. {
  292. return TargetVideoStream?.BitDepth;
  293. }
  294. var targetVideoCodecs = TargetVideoCodec;
  295. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  296. if (!string.IsNullOrEmpty(videoCodec))
  297. {
  298. return GetTargetVideoBitDepth(videoCodec);
  299. }
  300. return TargetVideoStream?.BitDepth;
  301. }
  302. }
  303. /// <summary>
  304. /// Gets the target reference frames that will be in the output stream.
  305. /// </summary>
  306. /// <value>The target reference frames.</value>
  307. public int? TargetRefFrames
  308. {
  309. get
  310. {
  311. if (IsDirectStream)
  312. {
  313. return TargetVideoStream?.RefFrames;
  314. }
  315. var targetVideoCodecs = TargetVideoCodec;
  316. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  317. if (!string.IsNullOrEmpty(videoCodec))
  318. {
  319. return GetTargetRefFrames(videoCodec);
  320. }
  321. return TargetVideoStream?.RefFrames;
  322. }
  323. }
  324. /// <summary>
  325. /// Gets the target framerate that will be in the output stream.
  326. /// </summary>
  327. /// <value>The target framerate.</value>
  328. public float? TargetFramerate
  329. {
  330. get
  331. {
  332. var stream = TargetVideoStream;
  333. return MaxFramerate.HasValue && !IsDirectStream
  334. ? MaxFramerate
  335. : stream?.ReferenceFrameRate;
  336. }
  337. }
  338. /// <summary>
  339. /// Gets the target video level that will be in the output stream.
  340. /// </summary>
  341. /// <value>The target video level.</value>
  342. public double? TargetVideoLevel
  343. {
  344. get
  345. {
  346. if (IsDirectStream)
  347. {
  348. return TargetVideoStream?.Level;
  349. }
  350. var targetVideoCodecs = TargetVideoCodec;
  351. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  352. if (!string.IsNullOrEmpty(videoCodec))
  353. {
  354. return GetTargetVideoLevel(videoCodec);
  355. }
  356. return TargetVideoStream?.Level;
  357. }
  358. }
  359. /// <summary>
  360. /// Gets the target packet length that will be in the output stream.
  361. /// </summary>
  362. /// <value>The target packet length.</value>
  363. public int? TargetPacketLength
  364. {
  365. get
  366. {
  367. var stream = TargetVideoStream;
  368. return !IsDirectStream
  369. ? null
  370. : stream?.PacketLength;
  371. }
  372. }
  373. /// <summary>
  374. /// Gets the target video profile that will be in the output stream.
  375. /// </summary>
  376. /// <value>The target video profile.</value>
  377. public string? TargetVideoProfile
  378. {
  379. get
  380. {
  381. if (IsDirectStream)
  382. {
  383. return TargetVideoStream?.Profile;
  384. }
  385. var targetVideoCodecs = TargetVideoCodec;
  386. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  387. if (!string.IsNullOrEmpty(videoCodec))
  388. {
  389. return GetOption(videoCodec, "profile");
  390. }
  391. return TargetVideoStream?.Profile;
  392. }
  393. }
  394. /// <summary>
  395. /// Gets the target video range type that will be in the output stream.
  396. /// </summary>
  397. /// <value>The video range type.</value>
  398. public VideoRangeType TargetVideoRangeType
  399. {
  400. get
  401. {
  402. if (IsDirectStream)
  403. {
  404. return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
  405. }
  406. var targetVideoCodecs = TargetVideoCodec;
  407. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  408. if (!string.IsNullOrEmpty(videoCodec)
  409. && Enum.TryParse(GetOption(videoCodec, "rangetype"), true, out VideoRangeType videoRangeType))
  410. {
  411. return videoRangeType;
  412. }
  413. return TargetVideoStream?.VideoRangeType ?? VideoRangeType.Unknown;
  414. }
  415. }
  416. /// <summary>
  417. /// Gets the target video codec tag.
  418. /// </summary>
  419. /// <value>The video codec tag.</value>
  420. public string? TargetVideoCodecTag
  421. {
  422. get
  423. {
  424. var stream = TargetVideoStream;
  425. return !IsDirectStream
  426. ? null
  427. : stream?.CodecTag;
  428. }
  429. }
  430. /// <summary>
  431. /// Gets the audio bitrate that will be in the output stream.
  432. /// </summary>
  433. /// <value>The audio bitrate.</value>
  434. public int? TargetAudioBitrate
  435. {
  436. get
  437. {
  438. var stream = TargetAudioStream;
  439. return AudioBitrate.HasValue && !IsDirectStream
  440. ? AudioBitrate
  441. : stream?.BitRate;
  442. }
  443. }
  444. /// <summary>
  445. /// Gets the amount of audio channels that will be in the output stream.
  446. /// </summary>
  447. /// <value>The target audio channels.</value>
  448. public int? TargetAudioChannels
  449. {
  450. get
  451. {
  452. if (IsDirectStream)
  453. {
  454. return TargetAudioStream?.Channels;
  455. }
  456. var targetAudioCodecs = TargetAudioCodec;
  457. var codec = targetAudioCodecs.Count == 0 ? null : targetAudioCodecs[0];
  458. if (!string.IsNullOrEmpty(codec))
  459. {
  460. return GetTargetRefFrames(codec);
  461. }
  462. return TargetAudioStream?.Channels;
  463. }
  464. }
  465. /// <summary>
  466. /// Gets the audio codec that will be in the output stream.
  467. /// </summary>
  468. /// <value>The audio codec.</value>
  469. public IReadOnlyList<string> TargetAudioCodec
  470. {
  471. get
  472. {
  473. var stream = TargetAudioStream;
  474. string? inputCodec = stream?.Codec;
  475. if (IsDirectStream)
  476. {
  477. return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
  478. }
  479. foreach (string codec in AudioCodecs)
  480. {
  481. if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
  482. {
  483. return string.IsNullOrEmpty(codec) ? [] : [codec];
  484. }
  485. }
  486. return AudioCodecs;
  487. }
  488. }
  489. /// <summary>
  490. /// Gets the video codec that will be in the output stream.
  491. /// </summary>
  492. /// <value>The target video codec.</value>
  493. public IReadOnlyList<string> TargetVideoCodec
  494. {
  495. get
  496. {
  497. var stream = TargetVideoStream;
  498. string? inputCodec = stream?.Codec;
  499. if (IsDirectStream)
  500. {
  501. return string.IsNullOrEmpty(inputCodec) ? [] : [inputCodec];
  502. }
  503. foreach (string codec in VideoCodecs)
  504. {
  505. if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
  506. {
  507. return string.IsNullOrEmpty(codec) ? [] : [codec];
  508. }
  509. }
  510. return VideoCodecs;
  511. }
  512. }
  513. /// <summary>
  514. /// Gets the target size of the output stream.
  515. /// </summary>
  516. /// <value>The target size.</value>
  517. public long? TargetSize
  518. {
  519. get
  520. {
  521. if (IsDirectStream)
  522. {
  523. return MediaSource?.Size;
  524. }
  525. if (RunTimeTicks.HasValue)
  526. {
  527. int? totalBitrate = TargetTotalBitrate;
  528. double totalSeconds = RunTimeTicks.Value;
  529. // Convert to ms
  530. totalSeconds /= 10000;
  531. // Convert to seconds
  532. totalSeconds /= 1000;
  533. return totalBitrate.HasValue ?
  534. Convert.ToInt64(totalBitrate.Value * totalSeconds) :
  535. null;
  536. }
  537. return null;
  538. }
  539. }
  540. /// <summary>
  541. /// Gets the target video bitrate of the output stream.
  542. /// </summary>
  543. /// <value>The video bitrate.</value>
  544. public int? TargetVideoBitrate
  545. {
  546. get
  547. {
  548. var stream = TargetVideoStream;
  549. return VideoBitrate.HasValue && !IsDirectStream
  550. ? VideoBitrate
  551. : stream?.BitRate;
  552. }
  553. }
  554. /// <summary>
  555. /// Gets the target timestamp of the output stream.
  556. /// </summary>
  557. /// <value>The target timestamp.</value>
  558. public TransportStreamTimestamp TargetTimestamp
  559. {
  560. get
  561. {
  562. var defaultValue = string.Equals(Container, "m2ts", StringComparison.OrdinalIgnoreCase)
  563. ? TransportStreamTimestamp.Valid
  564. : TransportStreamTimestamp.None;
  565. return !IsDirectStream
  566. ? defaultValue
  567. : MediaSource is null ? defaultValue : MediaSource.Timestamp ?? TransportStreamTimestamp.None;
  568. }
  569. }
  570. /// <summary>
  571. /// Gets the target total bitrate of the output stream.
  572. /// </summary>
  573. /// <value>The target total bitrate.</value>
  574. public int? TargetTotalBitrate => (TargetAudioBitrate ?? 0) + (TargetVideoBitrate ?? 0);
  575. /// <summary>
  576. /// Gets a value indicating whether the output stream is anamorphic.
  577. /// </summary>
  578. public bool? IsTargetAnamorphic
  579. {
  580. get
  581. {
  582. if (IsDirectStream)
  583. {
  584. return TargetVideoStream?.IsAnamorphic;
  585. }
  586. return false;
  587. }
  588. }
  589. /// <summary>
  590. /// Gets a value indicating whether the output stream is interlaced.
  591. /// </summary>
  592. public bool? IsTargetInterlaced
  593. {
  594. get
  595. {
  596. if (IsDirectStream)
  597. {
  598. return TargetVideoStream?.IsInterlaced;
  599. }
  600. var targetVideoCodecs = TargetVideoCodec;
  601. var videoCodec = targetVideoCodecs.Count == 0 ? null : targetVideoCodecs[0];
  602. if (!string.IsNullOrEmpty(videoCodec))
  603. {
  604. if (string.Equals(GetOption(videoCodec, "deinterlace"), "true", StringComparison.OrdinalIgnoreCase))
  605. {
  606. return false;
  607. }
  608. }
  609. return TargetVideoStream?.IsInterlaced;
  610. }
  611. }
  612. /// <summary>
  613. /// Gets a value indicating whether the output stream is AVC.
  614. /// </summary>
  615. public bool? IsTargetAVC
  616. {
  617. get
  618. {
  619. if (IsDirectStream)
  620. {
  621. return TargetVideoStream?.IsAVC;
  622. }
  623. return true;
  624. }
  625. }
  626. /// <summary>
  627. /// Gets the target width of the output stream.
  628. /// </summary>
  629. /// <value>The target width.</value>
  630. public int? TargetWidth
  631. {
  632. get
  633. {
  634. var videoStream = TargetVideoStream;
  635. if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
  636. {
  637. ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
  638. size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
  639. return size.Width;
  640. }
  641. return MaxWidth;
  642. }
  643. }
  644. /// <summary>
  645. /// Gets the target height of the output stream.
  646. /// </summary>
  647. /// <value>The target height.</value>
  648. public int? TargetHeight
  649. {
  650. get
  651. {
  652. var videoStream = TargetVideoStream;
  653. if (videoStream is not null && videoStream.Width.HasValue && videoStream.Height.HasValue)
  654. {
  655. ImageDimensions size = new ImageDimensions(videoStream.Width.Value, videoStream.Height.Value);
  656. size = DrawingUtils.Resize(size, 0, 0, MaxWidth ?? 0, MaxHeight ?? 0);
  657. return size.Height;
  658. }
  659. return MaxHeight;
  660. }
  661. }
  662. /// <summary>
  663. /// Gets the target video stream count of the output stream.
  664. /// </summary>
  665. /// <value>The target video stream count.</value>
  666. public int? TargetVideoStreamCount
  667. {
  668. get
  669. {
  670. if (IsDirectStream)
  671. {
  672. return GetMediaStreamCount(MediaStreamType.Video, int.MaxValue);
  673. }
  674. return GetMediaStreamCount(MediaStreamType.Video, 1);
  675. }
  676. }
  677. /// <summary>
  678. /// Gets the target audio stream count of the output stream.
  679. /// </summary>
  680. /// <value>The target audio stream count.</value>
  681. public int? TargetAudioStreamCount
  682. {
  683. get
  684. {
  685. if (IsDirectStream)
  686. {
  687. return GetMediaStreamCount(MediaStreamType.Audio, int.MaxValue);
  688. }
  689. return GetMediaStreamCount(MediaStreamType.Audio, 1);
  690. }
  691. }
  692. /// <summary>
  693. /// Sets a stream option.
  694. /// </summary>
  695. /// <param name="qualifier">The qualifier.</param>
  696. /// <param name="name">The name.</param>
  697. /// <param name="value">The value.</param>
  698. public void SetOption(string? qualifier, string name, string value)
  699. {
  700. if (string.IsNullOrEmpty(qualifier))
  701. {
  702. SetOption(name, value);
  703. }
  704. else
  705. {
  706. SetOption(qualifier + "-" + name, value);
  707. }
  708. }
  709. /// <summary>
  710. /// Sets a stream option.
  711. /// </summary>
  712. /// <param name="name">The name.</param>
  713. /// <param name="value">The value.</param>
  714. public void SetOption(string name, string value)
  715. {
  716. StreamOptions[name] = value;
  717. }
  718. /// <summary>
  719. /// Gets a stream option.
  720. /// </summary>
  721. /// <param name="qualifier">The qualifier.</param>
  722. /// <param name="name">The name.</param>
  723. /// <returns>The value.</returns>
  724. public string? GetOption(string? qualifier, string name)
  725. {
  726. var value = GetOption(qualifier + "-" + name);
  727. if (string.IsNullOrEmpty(value))
  728. {
  729. value = GetOption(name);
  730. }
  731. return value;
  732. }
  733. /// <summary>
  734. /// Gets a stream option.
  735. /// </summary>
  736. /// <param name="name">The name.</param>
  737. /// <returns>The value.</returns>
  738. public string? GetOption(string name)
  739. {
  740. if (StreamOptions.TryGetValue(name, out var value))
  741. {
  742. return value;
  743. }
  744. return null;
  745. }
  746. /// <summary>
  747. /// Returns this output stream URL for this class.
  748. /// </summary>
  749. /// <param name="baseUrl">The base Url.</param>
  750. /// <param name="accessToken">The access Token.</param>
  751. /// <returns>A querystring representation of this object.</returns>
  752. public string ToUrl(string baseUrl, string? accessToken)
  753. {
  754. ArgumentException.ThrowIfNullOrEmpty(baseUrl);
  755. List<string> list = [];
  756. foreach (NameValuePair pair in BuildParams(this, accessToken))
  757. {
  758. if (string.IsNullOrEmpty(pair.Value))
  759. {
  760. continue;
  761. }
  762. // Try to keep the url clean by omitting defaults
  763. if (string.Equals(pair.Name, "StartTimeTicks", StringComparison.OrdinalIgnoreCase)
  764. && string.Equals(pair.Value, "0", StringComparison.OrdinalIgnoreCase))
  765. {
  766. continue;
  767. }
  768. if (string.Equals(pair.Name, "SubtitleStreamIndex", StringComparison.OrdinalIgnoreCase)
  769. && string.Equals(pair.Value, "-1", StringComparison.OrdinalIgnoreCase))
  770. {
  771. continue;
  772. }
  773. if (string.Equals(pair.Name, "Static", StringComparison.OrdinalIgnoreCase)
  774. && string.Equals(pair.Value, "false", StringComparison.OrdinalIgnoreCase))
  775. {
  776. continue;
  777. }
  778. var encodedValue = pair.Value.Replace(" ", "%20", StringComparison.Ordinal);
  779. list.Add(string.Format(CultureInfo.InvariantCulture, "{0}={1}", pair.Name, encodedValue));
  780. }
  781. string queryString = string.Join('&', list);
  782. return GetUrl(baseUrl, queryString);
  783. }
  784. private string GetUrl(string baseUrl, string queryString)
  785. {
  786. ArgumentException.ThrowIfNullOrEmpty(baseUrl);
  787. string extension = string.IsNullOrEmpty(Container) ? string.Empty : "." + Container;
  788. baseUrl = baseUrl.TrimEnd('/');
  789. if (MediaType == DlnaProfileType.Audio)
  790. {
  791. if (SubProtocol == MediaStreamProtocol.hls)
  792. {
  793. return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
  794. }
  795. return string.Format(CultureInfo.InvariantCulture, "{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
  796. }
  797. if (SubProtocol == MediaStreamProtocol.hls)
  798. {
  799. return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
  800. }
  801. return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
  802. }
  803. private static List<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
  804. {
  805. List<NameValuePair> list = [];
  806. string audioCodecs = item.AudioCodecs.Count == 0 ?
  807. string.Empty :
  808. string.Join(',', item.AudioCodecs);
  809. string videoCodecs = item.VideoCodecs.Count == 0 ?
  810. string.Empty :
  811. string.Join(',', item.VideoCodecs);
  812. list.Add(new NameValuePair("DeviceProfileId", item.DeviceProfileId ?? string.Empty));
  813. list.Add(new NameValuePair("DeviceId", item.DeviceId ?? string.Empty));
  814. list.Add(new NameValuePair("MediaSourceId", item.MediaSourceId ?? string.Empty));
  815. list.Add(new NameValuePair("Static", item.IsDirectStream.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  816. list.Add(new NameValuePair("VideoCodec", videoCodecs));
  817. list.Add(new NameValuePair("AudioCodec", audioCodecs));
  818. list.Add(new NameValuePair("AudioStreamIndex", item.AudioStreamIndex.HasValue ? item.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  819. list.Add(new NameValuePair("SubtitleStreamIndex", item.SubtitleStreamIndex.HasValue && (item.AlwaysBurnInSubtitleWhenTranscoding || item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External) ? item.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  820. list.Add(new NameValuePair("VideoBitrate", item.VideoBitrate.HasValue ? item.VideoBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  821. list.Add(new NameValuePair("AudioBitrate", item.AudioBitrate.HasValue ? item.AudioBitrate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  822. list.Add(new NameValuePair("AudioSampleRate", item.AudioSampleRate.HasValue ? item.AudioSampleRate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  823. list.Add(new NameValuePair("MaxFramerate", item.MaxFramerate.HasValue ? item.MaxFramerate.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  824. list.Add(new NameValuePair("MaxWidth", item.MaxWidth.HasValue ? item.MaxWidth.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  825. list.Add(new NameValuePair("MaxHeight", item.MaxHeight.HasValue ? item.MaxHeight.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  826. long startPositionTicks = item.StartPositionTicks;
  827. if (item.SubProtocol == MediaStreamProtocol.hls)
  828. {
  829. list.Add(new NameValuePair("StartTimeTicks", string.Empty));
  830. }
  831. else
  832. {
  833. list.Add(new NameValuePair("StartTimeTicks", startPositionTicks.ToString(CultureInfo.InvariantCulture)));
  834. }
  835. list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
  836. list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
  837. string? liveStreamId = item.MediaSource?.LiveStreamId;
  838. list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
  839. list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
  840. if (!item.IsDirectStream)
  841. {
  842. if (item.RequireNonAnamorphic)
  843. {
  844. list.Add(new NameValuePair("RequireNonAnamorphic", item.RequireNonAnamorphic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  845. }
  846. list.Add(new NameValuePair("TranscodingMaxAudioChannels", item.TranscodingMaxAudioChannels.HasValue ? item.TranscodingMaxAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : string.Empty));
  847. if (item.EnableSubtitlesInManifest)
  848. {
  849. list.Add(new NameValuePair("EnableSubtitlesInManifest", item.EnableSubtitlesInManifest.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  850. }
  851. if (item.EnableMpegtsM2TsMode)
  852. {
  853. list.Add(new NameValuePair("EnableMpegtsM2TsMode", item.EnableMpegtsM2TsMode.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  854. }
  855. if (item.EstimateContentLength)
  856. {
  857. list.Add(new NameValuePair("EstimateContentLength", item.EstimateContentLength.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  858. }
  859. if (item.TranscodeSeekInfo != TranscodeSeekInfo.Auto)
  860. {
  861. list.Add(new NameValuePair("TranscodeSeekInfo", item.TranscodeSeekInfo.ToString().ToLowerInvariant()));
  862. }
  863. if (item.CopyTimestamps)
  864. {
  865. list.Add(new NameValuePair("CopyTimestamps", item.CopyTimestamps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  866. }
  867. list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  868. list.Add(new NameValuePair("EnableAudioVbrEncoding", item.EnableAudioVbrEncoding.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
  869. }
  870. list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
  871. string subtitleCodecs = item.SubtitleCodecs.Count == 0 ?
  872. string.Empty :
  873. string.Join(",", item.SubtitleCodecs);
  874. list.Add(new NameValuePair("SubtitleCodec", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed ? subtitleCodecs : string.Empty));
  875. if (item.SubProtocol == MediaStreamProtocol.hls)
  876. {
  877. list.Add(new NameValuePair("SegmentContainer", item.Container ?? string.Empty));
  878. if (item.SegmentLength.HasValue)
  879. {
  880. list.Add(new NameValuePair("SegmentLength", item.SegmentLength.Value.ToString(CultureInfo.InvariantCulture)));
  881. }
  882. if (item.MinSegments.HasValue)
  883. {
  884. list.Add(new NameValuePair("MinSegments", item.MinSegments.Value.ToString(CultureInfo.InvariantCulture)));
  885. }
  886. list.Add(new NameValuePair("BreakOnNonKeyFrames", item.BreakOnNonKeyFrames.ToString(CultureInfo.InvariantCulture)));
  887. }
  888. foreach (var pair in item.StreamOptions)
  889. {
  890. if (string.IsNullOrEmpty(pair.Value))
  891. {
  892. continue;
  893. }
  894. // strip spaces to avoid having to encode h264 profile names
  895. list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
  896. }
  897. if (!item.IsDirectStream)
  898. {
  899. list.Add(new NameValuePair("TranscodeReasons", item.TranscodeReasons.ToString()));
  900. }
  901. return list;
  902. }
  903. /// <summary>
  904. /// Gets the subtitle profiles.
  905. /// </summary>
  906. /// <param name="transcoderSupport">The transcoder support.</param>
  907. /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
  908. /// <param name="baseUrl">The base URL.</param>
  909. /// <param name="accessToken">The access token.</param>
  910. /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
  911. public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
  912. {
  913. return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
  914. }
  915. /// <summary>
  916. /// Gets the subtitle profiles.
  917. /// </summary>
  918. /// <param name="transcoderSupport">The transcoder support.</param>
  919. /// <param name="includeSelectedTrackOnly">If only the selected track should be included.</param>
  920. /// <param name="enableAllProfiles">If all profiles are enabled.</param>
  921. /// <param name="baseUrl">The base URL.</param>
  922. /// <param name="accessToken">The access token.</param>
  923. /// <returns>The <see cref="SubtitleStreamInfo"/> of the profiles.</returns>
  924. public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
  925. {
  926. if (MediaSource is null)
  927. {
  928. return [];
  929. }
  930. List<SubtitleStreamInfo> list = [];
  931. // HLS will preserve timestamps so we can just grab the full subtitle stream
  932. long startPositionTicks = SubProtocol == MediaStreamProtocol.hls
  933. ? 0
  934. : (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
  935. // First add the selected track
  936. if (SubtitleStreamIndex.HasValue)
  937. {
  938. foreach (var stream in MediaSource.MediaStreams)
  939. {
  940. if (stream.Type == MediaStreamType.Subtitle && stream.Index == SubtitleStreamIndex.Value)
  941. {
  942. AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
  943. }
  944. }
  945. }
  946. if (!includeSelectedTrackOnly)
  947. {
  948. foreach (var stream in MediaSource.MediaStreams)
  949. {
  950. if (stream.Type == MediaStreamType.Subtitle && (!SubtitleStreamIndex.HasValue || stream.Index != SubtitleStreamIndex.Value))
  951. {
  952. AddSubtitleProfiles(list, stream, transcoderSupport, enableAllProfiles, baseUrl, accessToken, startPositionTicks);
  953. }
  954. }
  955. }
  956. return list;
  957. }
  958. private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
  959. {
  960. if (enableAllProfiles)
  961. {
  962. foreach (var profile in DeviceProfile.SubtitleProfiles)
  963. {
  964. var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
  965. if (info is not null)
  966. {
  967. list.Add(info);
  968. }
  969. }
  970. }
  971. else
  972. {
  973. var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
  974. if (info is not null)
  975. {
  976. list.Add(info);
  977. }
  978. }
  979. }
  980. private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
  981. {
  982. if (MediaSource is null)
  983. {
  984. return null;
  985. }
  986. var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
  987. var info = new SubtitleStreamInfo
  988. {
  989. IsForced = stream.IsForced,
  990. Language = stream.Language,
  991. Name = stream.Language ?? "Unknown",
  992. Format = subtitleProfile.Format,
  993. Index = stream.Index,
  994. DeliveryMethod = subtitleProfile.Method,
  995. DisplayTitle = stream.DisplayTitle
  996. };
  997. if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
  998. {
  999. if (MediaSource.Protocol == MediaProtocol.File || !string.Equals(stream.Codec, subtitleProfile.Format, StringComparison.OrdinalIgnoreCase) || !stream.IsExternal)
  1000. {
  1001. info.Url = string.Format(
  1002. CultureInfo.InvariantCulture,
  1003. "{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
  1004. baseUrl,
  1005. ItemId,
  1006. MediaSourceId,
  1007. stream.Index.ToString(CultureInfo.InvariantCulture),
  1008. startPositionTicks.ToString(CultureInfo.InvariantCulture),
  1009. subtitleProfile.Format);
  1010. if (!string.IsNullOrEmpty(accessToken))
  1011. {
  1012. info.Url += "?api_key=" + accessToken;
  1013. }
  1014. info.IsExternalUrl = false;
  1015. }
  1016. else
  1017. {
  1018. info.Url = stream.Path;
  1019. info.IsExternalUrl = true;
  1020. }
  1021. }
  1022. return info;
  1023. }
  1024. /// <summary>
  1025. /// Gets the target video bit depth.
  1026. /// </summary>
  1027. /// <param name="codec">The codec.</param>
  1028. /// <returns>The target video bit depth.</returns>
  1029. public int? GetTargetVideoBitDepth(string? codec)
  1030. {
  1031. var value = GetOption(codec, "videobitdepth");
  1032. if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
  1033. {
  1034. return result;
  1035. }
  1036. return null;
  1037. }
  1038. /// <summary>
  1039. /// Gets the target audio bit depth.
  1040. /// </summary>
  1041. /// <param name="codec">The codec.</param>
  1042. /// <returns>The target audio bit depth.</returns>
  1043. public int? GetTargetAudioBitDepth(string? codec)
  1044. {
  1045. var value = GetOption(codec, "audiobitdepth");
  1046. if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
  1047. {
  1048. return result;
  1049. }
  1050. return null;
  1051. }
  1052. /// <summary>
  1053. /// Gets the target video level.
  1054. /// </summary>
  1055. /// <param name="codec">The codec.</param>
  1056. /// <returns>The target video level.</returns>
  1057. public double? GetTargetVideoLevel(string? codec)
  1058. {
  1059. var value = GetOption(codec, "level");
  1060. if (double.TryParse(value, CultureInfo.InvariantCulture, out var result))
  1061. {
  1062. return result;
  1063. }
  1064. return null;
  1065. }
  1066. /// <summary>
  1067. /// Gets the target reference frames.
  1068. /// </summary>
  1069. /// <param name="codec">The codec.</param>
  1070. /// <returns>The target reference frames.</returns>
  1071. public int? GetTargetRefFrames(string? codec)
  1072. {
  1073. var value = GetOption(codec, "maxrefframes");
  1074. if (int.TryParse(value, CultureInfo.InvariantCulture, out var result))
  1075. {
  1076. return result;
  1077. }
  1078. return null;
  1079. }
  1080. /// <summary>
  1081. /// Gets the target audio channels.
  1082. /// </summary>
  1083. /// <param name="codec">The codec.</param>
  1084. /// <returns>The target audio channels.</returns>
  1085. public int? GetTargetAudioChannels(string? codec)
  1086. {
  1087. var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
  1088. var value = GetOption(codec, "audiochannels");
  1089. if (string.IsNullOrEmpty(value))
  1090. {
  1091. return defaultValue;
  1092. }
  1093. if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
  1094. {
  1095. return Math.Min(result, defaultValue ?? result);
  1096. }
  1097. return defaultValue;
  1098. }
  1099. /// <summary>
  1100. /// Gets the media stream count.
  1101. /// </summary>
  1102. /// <param name="type">The type.</param>
  1103. /// <param name="limit">The limit.</param>
  1104. /// <returns>The media stream count.</returns>
  1105. private int? GetMediaStreamCount(MediaStreamType type, int limit)
  1106. {
  1107. var count = MediaSource?.GetStreamCount(type);
  1108. if (count.HasValue)
  1109. {
  1110. count = Math.Min(count.Value, limit);
  1111. }
  1112. return count;
  1113. }
  1114. }