Utility.ets 67 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125
  1. import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
  2. import {
  3. AppUtil,
  4. ArrayUtil,
  5. Base64Util,
  6. DateUtil, FileUtil, ImageUtil, LogUtil,
  7. MD5,
  8. PreferencesUtil,
  9. RandomUtil, StrUtil } from '@pura/harmony-utils';
  10. import { media } from '@kit.MediaKit';
  11. import fs, { ReadTextOptions } from '@ohos.file.fs';
  12. import { image } from '@kit.ImageKit';
  13. import fileIo from '@ohos.file.fs';
  14. import { VideoItem } from '../../viewmodel/VideoItem';
  15. import { photoAccessHelper } from '@kit.MediaLibraryKit';
  16. import { dataSharePredicates, uniformTypeDescriptor } from '@kit.ArkData';
  17. import { systemShare } from '@kit.ShareKit';
  18. import { fileUri, picker } from '@kit.CoreFileKit';
  19. import { common, UIAbility, Want } from '@kit.AbilityKit';
  20. import { CommonConstants, STR_LOCK_VIDEO, VIP_FILEPATH } from '../constants/CommonConstants';
  21. import { window } from '@kit.ArkUI';
  22. import { bundleManager } from '@kit.AbilityKit'
  23. import { pinyin4js } from '@ohos/pinyin4js';
  24. import { VipData } from '../../viewmodel/VipData';
  25. import { VipPage } from '../../pages/VipPage';
  26. import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
  27. export interface FFMpegTags {
  28. album?: string;
  29. ALBUM?: string;
  30. artist?: string;
  31. ARTIST?: string;
  32. TITLE?: string;
  33. title?: string;
  34. track?: string;
  35. TRACK?: string;
  36. TYER?: string;
  37. year?: string;
  38. genre?:string;
  39. GENRE?:string;
  40. date?: string;
  41. DATE?: string;
  42. LYRICS?: string;
  43. lyrics?: string; // 小写变体
  44. USLT?: string; // ID3v2同步歌词
  45. UNSYNCEDLYRICS?: string; // ID3v2非同步歌词
  46. // Add any other tag properties you expect
  47. ALBUMARTIST?:string//专辑艺术家
  48. COMPOSER?:string//作曲家
  49. LYRICIST?:string//作词家
  50. COMMENT?:string//注释
  51. albumartist?:string//专辑艺术家
  52. album_artist?:string//专辑艺术家
  53. TPE2?:string//专辑艺术家
  54. composer?:string//作曲家
  55. lyricist?:string//作词家
  56. TEXT?:string//作词家
  57. comment?:string//注释
  58. COMM?:string//注释
  59. comm?:string//注释
  60. disc?:string//音碟号
  61. }
  62. interface FFprobeFormat {
  63. filename: string;
  64. nb_streams: number;
  65. nb_programs: number;
  66. format_name: string;
  67. format_long_name: string;
  68. duration: string;
  69. size: string;
  70. bit_rate: string;
  71. probe_score: number;
  72. tags?: FFMpegTags;
  73. }
  74. interface FFprobeStream {
  75. // Define stream properties as needed
  76. codec_type?: string; // 流类型,如"audio"、"video"
  77. sample_rate?: string; // 采样率
  78. bit_rate?: string; // 比特率
  79. disposition?: StreamDisposition; // 添加disposition属性
  80. bits_per_raw_sample?: string; // 位深
  81. channels?: string; // 声道数
  82. channel_layout?: string; // 声道布局
  83. start_time?: string; // 起始播放
  84. }
  85. interface StreamDisposition {
  86. default?: number;
  87. dub?: number;
  88. original?: number;
  89. comment?: number;
  90. lyrics?: number;
  91. karaoke?: number;
  92. forced?: number;
  93. hearing_impaired?: number;
  94. visual_impaired?: number;
  95. clean_effects?: number;
  96. attached_pic?: number; // 添加封面图片标识
  97. timed_thumbnails?: number;
  98. captions?: number;
  99. descriptions?: number;
  100. metadata?: number;
  101. dependent?: number;
  102. still_image?: number;
  103. }
  104. interface FFprobeMetadata {
  105. streams: FFprobeStream[];
  106. format: FFprobeFormat;
  107. }
  108. export class Utility {
  109. private constructor() {}
  110. static isOpenTime():boolean{
  111. const currentDate = new Date();
  112. const targetDate = new Date(CommonConstants.OPEN_DATE);
  113. if (currentDate > targetDate) {
  114. return true
  115. }
  116. return false
  117. }
  118. static readonly FOREVER_DATE: string = 'forever';
  119. /**
  120. * 是否会员(只看线上会员状态)
  121. */
  122. static isNoble(): boolean {
  123. // 只判断线上会员状态
  124. return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  125. }
  126. static isForever(): boolean {
  127. // 判断是不是永久会员
  128. return PreferencesUtil.getBooleanSync('isForever', false);
  129. }
  130. /**
  131. * 旧版客户端判断会员状态
  132. * @returns
  133. */
  134. static isNobleForOld():boolean{
  135. if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。
  136. return true
  137. return PreferencesUtil.getBooleanSync('isNoble',false)
  138. }
  139. /**
  140. * setNoble方法废弃,保留兼容但不做任何操作
  141. */
  142. static setNoble(expireDate:string){
  143. let isNoble = false
  144. if(StrUtil.isNotEmpty(expireDate)){
  145. if(expireDate===VipPage.FOREVER_DATE){
  146. isNoble = true
  147. }else{
  148. const currentDate = new Date();
  149. const targetDate = DateUtil.getFormatDate(expireDate)
  150. if (currentDate > targetDate) {
  151. isNoble = false
  152. }else{
  153. isNoble = true
  154. }
  155. }
  156. }
  157. PreferencesUtil.putSync('isNoble',isNoble)
  158. }
  159. /**
  160. * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800
  161. * @param day
  162. * @returns
  163. */
  164. static isPassInstallTime(day:number):boolean{
  165. //获取当前时间
  166. let currentTime = new Date().getTime();
  167. // 尝试从存储中获取安装时间
  168. let installTime = getInstallTime();
  169. if (installTime===0||installTime === null|| installTime===undefined) {
  170. // 如果没有存储安装时间,则存储当前时间为安装时间
  171. setInstallTime(currentTime);
  172. return false
  173. } else {
  174. // 计算时间差(以分钟为单位)
  175. const timeDifference = (currentTime - installTime) / (1000 * 60);
  176. LogUtil.debug("onecold timeDifference=" + timeDifference)
  177. // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800
  178. if (timeDifference > 24*60*day) {
  179. return true
  180. }else{
  181. return false
  182. }
  183. }
  184. }
  185. // 定义一个函数来获取当前的时间段
  186. static getTimePeriod(): string {
  187. const now = new Date();
  188. const hours = now.getHours();
  189. if (hours >= 5 && hours < 12) {
  190. return "早上好";
  191. } else if (hours >= 12 && hours < 18) {
  192. return "下午好";
  193. } else if (hours >= 18 && hours < 22) {
  194. return "晚上好";
  195. } else {
  196. return "夜深了";
  197. }
  198. }
  199. static optimizedFormat(speed: number): string {
  200. if (speed) {
  201. const str = speed.toFixed(2);
  202. let end = str.length;
  203. while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) {
  204. end--;
  205. if (str[end] === '.') {
  206. break;
  207. }
  208. }
  209. return str.slice(0, end || 1) + 'x';
  210. } else {
  211. return '1x'
  212. }
  213. }
  214. static isHaoOpenTime():boolean{
  215. const currentDate = new Date();
  216. const targetDate = new Date('2025-04-20');
  217. if (currentDate < targetDate) {
  218. return true
  219. }
  220. return false
  221. }
  222. static addDays(date: Date, days: number): Date {
  223. const result = new Date(date);
  224. result.setDate(result.getDate() + days);
  225. return result;
  226. }
  227. static convertToKHz(sampleRateHz: string|undefined): string {
  228. if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined)
  229. return '0KHz'
  230. const sampleRateKHz = Number(sampleRateHz) / 1000;
  231. return `${sampleRateKHz} KHz`;
  232. }
  233. /**
  234. * 格式化媒体格式类型显示
  235. * @param mimeType 媒体格式类型字符串
  236. * @return 格式化后的显示字符串
  237. */
  238. static formatMimeType(mimeType: string | undefined): string {
  239. if (StrUtil.isEmpty(mimeType) || mimeType === undefined) {
  240. return 'unknown';
  241. }
  242. // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3"
  243. try {
  244. const parts = mimeType.split('/');
  245. if (parts.length > 1) {
  246. return parts[1].toUpperCase();
  247. } else {
  248. return mimeType.toUpperCase();
  249. }
  250. } catch (err) {
  251. console.error(`格式化媒体类型出错: ${err}`);
  252. return 'unknown';
  253. }
  254. }
  255. //根据字节获取大小
  256. static formatFileSize(bytes:number) {
  257. const units = ['Bytes', 'Kbps', 'Mbps'];
  258. let size = bytes;
  259. let unitIndex = 0;
  260. while (size >= 1024 && unitIndex < units.length - 1) {
  261. size /= 1024;
  262. unitIndex++;
  263. }
  264. if(size > 1024&&unitIndex==2){
  265. size = size*0.1
  266. }
  267. if(size > 1024&&unitIndex==2){
  268. size = size*0.1
  269. }
  270. if(size > 480&&unitIndex==2){
  271. size = size*0.5
  272. }
  273. // 保留两位小数, 四舍五入
  274. size = Math.round(size * 10) / 10;
  275. return size + units[unitIndex]
  276. }
  277. //根据字节获取大小无单位
  278. static formatFileSizeWithout(bytes:number) {
  279. const units = ['Bytes', 'Kbps', 'Mbps'];
  280. let size = bytes;
  281. let unitIndex = 0;
  282. while (size >= 1024 && unitIndex < units.length - 1) {
  283. size /= 1024;
  284. unitIndex++;
  285. }
  286. if(size > 1024&&unitIndex==2){
  287. size = size*0.1
  288. }
  289. if(size > 1024&&unitIndex==2){
  290. size = size*0.1
  291. }
  292. if(size > 480&&unitIndex==2){
  293. size = size*0.5
  294. }
  295. // 保留两位小数, 四舍五入
  296. size = Math.round(size * 10) / 10;
  297. return size
  298. }
  299. static copyText(text:string):boolean{
  300. const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text)
  301. const systemPasteboard = pasteboard.getSystemPasteboard()
  302. systemPasteboard.setData(pasteboardData)
  303. systemPasteboard.getData().then((data)=>{
  304. return data
  305. })
  306. return false
  307. }
  308. static getDbm(rssi:number):number{
  309. return Math.round((rssi / 2) + 100)
  310. }
  311. //根据数字形式的IP地址获取字符串形式的IP地址
  312. static getIpAddrFromNum(ipNum: number): string {
  313. return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
  314. }
  315. static resolveIP(ip:number) {
  316. let address: string = ip.toString()
  317. if (address === '0') {
  318. return '00:00:000:000'
  319. }
  320. address.substring(0, 2)
  321. return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}`
  322. }
  323. static isFreeTime():boolean{
  324. if(DateUtil.isWeekend()){
  325. return true
  326. }
  327. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  328. let a = Number(str);
  329. if(isNaN(a))
  330. return false
  331. // ToastUtil.showToast('a='+a)
  332. if (a >= 7 && a <13) {//白天
  333. return false;
  334. }
  335. if (a >= 13 && a <20) {//白天
  336. return false;
  337. }
  338. if (a >= 0 && a < 7) {//凌晨
  339. return true;
  340. }
  341. if (a >= 20 && a <= 24) {//晚上
  342. return true;
  343. }
  344. return false
  345. }
  346. //判断是否是音乐文件
  347. static isMusicByExtension(filename:string) {
  348. const extensions = CommonConstants.REAL_MUSIC_FORMAT
  349. const lastIndex = filename.lastIndexOf('.');
  350. if (lastIndex!== -1) {
  351. const fileExtension = filename.slice(lastIndex).toLowerCase();
  352. return extensions.includes(fileExtension);
  353. }
  354. return false;
  355. }
  356. //判断是否是媒体文件 音乐和视频都可以
  357. static isMeidaByExtension(filename:string) {
  358. const extensions = CommonConstants.MEDIA_FORMAT
  359. const lastIndex = filename.lastIndexOf('.');
  360. if (lastIndex!== -1) {
  361. const fileExtension = filename.slice(lastIndex).toLowerCase();
  362. return extensions.includes(fileExtension);
  363. }
  364. return false;
  365. }
  366. //判断是否是视频文件
  367. static isVideoByExtension(filename:string) {
  368. const extensions = CommonConstants.VIDEO_FORMAT
  369. const lastIndex = filename.lastIndexOf('.');
  370. if (lastIndex!== -1) {
  371. const fileExtension = filename.slice(lastIndex).toLowerCase();
  372. return extensions.includes(fileExtension);
  373. }
  374. return false;
  375. }
  376. // 获取缩略图
  377. static async getFetchFrameByTime(filePath: string,time?:number,imageWidth?:number,imageHeight?:number) {
  378. if(Utility.isMusicByExtension(filePath)){
  379. return undefined
  380. }
  381. let pixelMap:image.PixelMap|undefined =undefined;
  382. try{
  383. // 创建AVImageGenerator对象
  384. let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
  385. let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  386. let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
  387. avImageGenerator.fdSrc = avFileDescriptor;
  388. // 初始化入参
  389. let timeUs = 0
  390. console.info('onecold time='+time)
  391. if(time){
  392. timeUs = (time > 0) ? time*50 : 0
  393. }
  394. let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
  395. if(imageWidth){
  396. imageWidth =imageWidth/2.5
  397. }
  398. if(imageHeight){
  399. imageHeight = imageHeight/2.5
  400. }
  401. let param: media.PixelMapParams = {
  402. width : imageWidth||300,
  403. height : imageHeight||400,
  404. }
  405. // 获取缩略图(promise模式)
  406. pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
  407. // 释放资源(promise模式)
  408. avImageGenerator.release()
  409. console.info(`release success.`)
  410. fs.closeSync(file);
  411. }catch (error) {
  412. console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
  413. }
  414. return pixelMap
  415. }
  416. // 获取fd文件路径
  417. static async getFdDir(path:string){
  418. let fdPath = 'fd://';
  419. let file = await fileIo.open(path)
  420. fdPath = fdPath + '' + file.fd;
  421. return fdPath
  422. }
  423. // 根据uri获取文件名称
  424. static getMediaNameByUri(myUri: string) {
  425. let myFileName = (myUri.split('/').pop()) as string;
  426. return decodeURIComponent(myFileName)
  427. }
  428. static msToStandardTime(ms:string):string{
  429. let date = new Date(ms);
  430. let hours = date.getHours();
  431. let minutes = date.getMinutes();
  432. let seconds = date.getSeconds();
  433. return hours + ':' + minutes + ':' + seconds;
  434. }
  435. static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number {
  436. // 定义正则表达式以匹配日期字符串中的各部分
  437. const regex = format
  438. .replace('yyyy', '(\\d{4})')
  439. .replace('MM', '(\\d{2})')
  440. .replace('dd', '(\\d{2})')
  441. .replace('HH', '(\\d{2})')
  442. .replace('mm', '(\\d{2})');
  443. const match = new RegExp(regex).exec(dateStr);
  444. if (!match) {
  445. throw new Error('Invalid date string or format');
  446. }
  447. // 解析匹配结果
  448. const year = Number(match[1]);
  449. const month = Number(match[2]);
  450. const day = Number(match[3]);
  451. const hours = Number(match[4]);
  452. const minutes = Number(match[5]);
  453. // 创建 Date 对象
  454. const dateObj = new Date(year, month - 1, day, hours, minutes);
  455. // 返回时间戳(以秒为单位)
  456. return Math.floor(dateObj.getTime() / 1000);
  457. }
  458. static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string {
  459. // 将输入转换为 Date 对象
  460. let dateObj: Date;
  461. if (typeof date === 'number') {
  462. // 如果是十位时间戳,转换为毫秒数
  463. dateObj = new Date(date.toString().length === 10 ? date * 1000 : date);
  464. } else if (typeof date === 'string') {
  465. // 如果是字符串,尝试解析为 Date 对象
  466. dateObj = new Date(date);
  467. } else {
  468. // 如果是 Date 对象,直接使用
  469. dateObj = date;
  470. }
  471. // 定义替换规则
  472. const replacements = new Map<string, string>([
  473. ['yyyy', String(dateObj.getFullYear())],
  474. ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')],
  475. ['dd', String(dateObj.getDate()).padStart(2, '0')],
  476. ['HH', String(dateObj.getHours()).padStart(2, '0')],
  477. ['mm', String(dateObj.getMinutes()).padStart(2, '0')],
  478. ['ss', String(dateObj.getSeconds()).padStart(2, '0')]
  479. ]);
  480. // 替换格式字符串中的占位符
  481. replacements.forEach((value, key) => {
  482. format = format.replace(new RegExp(key, 'g'), value);
  483. });
  484. return format;
  485. }
  486. static async getFilePixelMapBig(uri:string){
  487. let pixelMap:image.PixelMap|undefined = undefined
  488. if(Utility.isMusicByExtension(uri)){
  489. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  490. }else{
  491. pixelMap = await Utility.getFetchFrameByTime(uri)
  492. }
  493. return ImageUtil.pixelMapToBase64StrBig(pixelMap)
  494. }
  495. static async getFilePixelMap(uri:string){
  496. let pixelMap:image.PixelMap|undefined = undefined
  497. if(Utility.isMusicByExtension(uri)){
  498. pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri)
  499. }else{
  500. pixelMap = await Utility.getFetchFrameByTime(uri)
  501. }
  502. return ImageUtil.pixelMapToBase64Str(pixelMap)
  503. }
  504. //根据filePathe获取对应的播放Index
  505. static getIndexFromList(localList:Array<VideoItem>,filePath:string){
  506. let list: Array<string> = [];
  507. for(let i=0;i<localList.length;i++){
  508. if(localList[i].filePath === filePath){
  509. return i
  510. }
  511. }
  512. return 0
  513. }
  514. // 在以下demo中,使用资源管理接口获取打包在HAP内的媒体资源文件,通过设置fdSrc属性,获取音频元数据并打印,
  515. // 获取音频专辑封面并通过Image控件显示在屏幕上。该demo以Promise形式进行异步接口调用
  516. static async getFetchMetadataFromFdSrcByPromise(uri: string): Promise<image.PixelMap | undefined> {
  517. let cover: image.PixelMap | undefined = undefined;
  518. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  519. try {
  520. // 创建AVMetadataExtractor对象
  521. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  522. // 设置fdSrc
  523. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  524. avMetadataExtractor.fdSrc = fd;
  525. // 获取元数据(promise模式)
  526. const metadata = await avMetadataExtractor.fetchMetadata();
  527. metadata.author
  528. console.info(`get meta data, hasAudio: ${metadata.hasAudio}`);
  529. // 获取专辑封面(promise模式)
  530. cover = await avMetadataExtractor.fetchAlbumCover();
  531. // 释放资源(promise模式)
  532. await avMetadataExtractor.release();
  533. console.info('release success.');
  534. } catch (error) {
  535. console.error('Error during metadata extraction:', error);
  536. }
  537. } else {
  538. console.warn('AVMetadataExtractor capability is not supported.');
  539. }
  540. return cover;
  541. }
  542. static formatTimestamp(timestamp: number): string {
  543. // 将时间戳转换为 Date 对象
  544. const date = new Date(timestamp);
  545. // 获取各个部分
  546. const year = date.getFullYear();
  547. const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
  548. const day = String(date.getDate()).padStart(2, '0');
  549. const hours = String(date.getHours()).padStart(2, '0');
  550. const minutes = String(date.getMinutes()).padStart(2, '0');
  551. const seconds = String(date.getSeconds()).padStart(2, '0');
  552. // 拼接成所需的格式
  553. return `${year}-${month}-${day} ${hours}:${minutes}`;
  554. }
  555. //获取音乐资源的属性值,
  556. static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
  557. if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
  558. return new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
  559. }
  560. return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
  561. //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav 不支持内嵌封面dsf,aif,aiff
  562. if(StrUtil.isNotEmpty(uri)){
  563. if(uri.toLowerCase().endsWith('.dsf')
  564. ||uri.toLowerCase().endsWith('.aif')
  565. // ||uri.toLowerCase().endsWith('.wav')
  566. ||uri.toLowerCase().endsWith('.aiff')){
  567. return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
  568. }
  569. }
  570. let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
  571. try {
  572. console.info('asset file.uri: ', uri);
  573. let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  574. console.info("file.fd " + file.fd);
  575. let fdfd = 'fd://' + file.fd
  576. //3、通过fs.stat方法获取stat对象
  577. console.info('asset file.name: ', file.name);
  578. console.info('asset file.uri: ', uri);
  579. console.info('asset file.fd: ', file.fd);
  580. console.info('asset file.path: ', file.path);
  581. item = new VideoItem(file.name,uri,uri,type,0,'')
  582. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  583. console.info("get file info succeed, the size of file is " + stat.size);
  584. let videoSize = stat.size
  585. // let videoTime = stat.ctime
  586. let fileSize = Utility.formatFSize(videoSize)
  587. //按照添加时间
  588. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  589. // console.info('onecold asset addTime: ', addTime);
  590. // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
  591. // console.info('asset stat.ctime: ', stat.ctime);
  592. let musicName:string | undefined = file.name
  593. let artist:string | undefined = ''
  594. let album:string | undefined = ''
  595. let pixelMap:image.PixelMap|undefined|null = undefined
  596. let imagePath = ''
  597. let duration:string | undefined = ''
  598. let mimeType:string | undefined = ''
  599. let trackCount:string | undefined = ''//轨道数量
  600. let sampleRate:string | undefined = ''//音频的采样率单位为Hz
  601. if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  602. try {
  603. // 创建AVMetadataExtractor对象
  604. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  605. // 设置fdSrc
  606. const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
  607. avMetadataExtractor.fdSrc = fd;
  608. // 获取元数据(promise模式)
  609. const metadata = await avMetadataExtractor.fetchMetadata();
  610. if(StrUtil.isNotEmpty(metadata.title)){
  611. musicName = metadata.title
  612. }else{
  613. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  614. console.log(`onecold musicName为空:${file.name}`);
  615. const musicData = parseMusicFileName(file.name);
  616. if (musicData.isValid) {
  617. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  618. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  619. musicName = musicData.title
  620. if(artist==''||artist==undefined)
  621. artist = musicData.artist
  622. } else {
  623. musicName = file.name
  624. // console.log("onecold 文件名格式不符合要求");
  625. }
  626. }
  627. if(StrUtil.isNotEmpty(metadata.artist)){
  628. artist = metadata.artist
  629. }
  630. if(artist==undefined)
  631. artist = ''
  632. if(StrUtil.isNotEmpty(metadata.album)){
  633. album = metadata.album
  634. }else{
  635. album = ''
  636. }
  637. if(StrUtil.isNotEmpty(metadata.duration)){
  638. if(metadata.duration)
  639. duration = convertSecondsToTime(metadata.duration.toString())
  640. }
  641. if(StrUtil.isNotEmpty(metadata.mimeType)){
  642. mimeType = metadata.mimeType
  643. }
  644. if(StrUtil.isNotEmpty(metadata.trackCount)){
  645. trackCount = metadata.trackCount
  646. }
  647. if(StrUtil.isNotEmpty(metadata.sampleRate)){
  648. sampleRate = metadata.sampleRate
  649. }
  650. let name = await MD5.digestSync(uri)
  651. // if(isLoadPixelMap){
  652. // 获取专辑封面(promise模式)
  653. // pixelMap = await avMetadataExtractor.fetchAlbumCover();
  654. // // 释放资源(promise模式)
  655. // await avMetadataExtractor.release();
  656. pixelMap = await fetchAlbumCover(avMetadataExtractor)
  657. // console.info('onecold release success. name= '+musicName);
  658. if(pixelMap!==undefined&&pixelMap!==null){
  659. // console.info('onecold pixelMap is not empty= '+musicName);
  660. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
  661. imagePath = fileUri.getUriFromPath(imagePath)
  662. }else{
  663. // console.info('onecold pixelMap is empty= '+musicName);
  664. imagePath = ''
  665. // if(StrUtil.isNotEmpty(artist))
  666. // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
  667. }
  668. // }else{
  669. // imagePath = context.filesDir + FileUtil.separator + name
  670. // imagePath = fileUri.getUriFromPath(imagePath)
  671. // }
  672. console.info('onecold release success. imagePath= '+imagePath);
  673. } catch (error) {
  674. console.error('Error during metadata extraction:', error);
  675. }
  676. } else {
  677. console.warn('AVMetadataExtractor capability is not supported.');
  678. }
  679. if(musicName==undefined)
  680. musicName = file.name
  681. item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,undefined,fileSize,
  682. imagePath,artist,album,file.name)
  683. item.duration = duration+'';
  684. item.mimeType = mimeType;
  685. item.trackCount = trackCount;
  686. item.sampleRate = sampleRate;
  687. item.isFav = 0;
  688. item.playCount = 0;
  689. item.pyStr = pinyin4js.getShortPinyin(musicName)
  690. // item.lyricContent = await extractLyricsContent(uri)
  691. let metaItem = await parseAudioMetadata(uri)
  692. if(metaItem){
  693. item.lyricContent = metaItem.lyricContent
  694. item.bit_rate = formatBitrateToKbps(metaItem.bit_rate || "0");
  695. item.year = metaItem.year ||'unknown'
  696. item.probe_score = metaItem.probe_score
  697. item.nb_streams = metaItem.nb_streams
  698. item.nb_programs = metaItem.nb_programs
  699. }
  700. })
  701. } catch (error) {
  702. console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
  703. }
  704. return item
  705. }
  706. static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,autoParseMusicName?:boolean): Promise<VideoItem> {
  707. return new Promise((resolve, reject) => {
  708. let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
  709. let outputJson = "";
  710. FFmpeg.execute(commands, {
  711. logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
  712. outputCallback: (message: string) => {
  713. outputJson += message;
  714. },
  715. }).then(async () => {
  716. try {
  717. const metadata: FFprobeMetadata = JSON.parse(outputJson);
  718. const format = metadata.format;
  719. console.log(`onecold outputJson:${outputJson}`);
  720. let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
  721. console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
  722. let videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
  723. await fs.stat(file.fd).then(async (stat: fs.Stat) => {
  724. // 获取音频流的采样率
  725. let sampleRate:string|undefined = '';
  726. let bits_per_raw_sample:string|undefined = '';
  727. let channels:string|undefined = '';
  728. let channel_layout:string|undefined = '';
  729. let start_time:string|undefined = '';
  730. if (metadata.streams && metadata.streams.length > 0) {
  731. // 查找第一个音频流
  732. const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
  733. if (audioStream) {
  734. sampleRate = audioStream.sample_rate;
  735. bits_per_raw_sample = audioStream.bits_per_raw_sample
  736. channel_layout = audioStream.channel_layout
  737. channels = audioStream.channels
  738. start_time = audioStream.start_time
  739. }
  740. }
  741. let videoSize = stat.size
  742. let fileSize = Utility.formatFSize(videoSize)
  743. //按照添加时间
  744. let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
  745. // Extract artist and title from tags
  746. const tags = format.tags || {};
  747. let artist = tags.artist ||tags.ARTIST || '';
  748. let title = tags.title ||tags.TITLE || '';
  749. let album = tags.album ||tags.ALBUM|| '';
  750. console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
  751. // 检查是否有乱码
  752. if (hasGarbledText(tags)) {
  753. // 第二次尝试用华为的接口提取元数据
  754. let hwTags = await extractHwMediaMetadata(inputPath)
  755. artist = hwTags?.artist|| '';
  756. title = hwTags?.title|| '';
  757. album = hwTags?.album|| '';
  758. console.info('onecold 乱码修正 artist='+artist);
  759. console.info('onecold 乱码修正 title='+title);
  760. console.info('onecold 乱码修正 album='+album);
  761. }
  762. if(StrUtil.isEmpty(title)){
  763. //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
  764. console.log(`onecold musicName为空:${file.name}`);
  765. // let autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName',false)
  766. if(autoParseMusicName){
  767. const musicData = parseMusicFileName(file.name);
  768. if (musicData.isValid) {
  769. // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
  770. // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
  771. title = musicData.title
  772. if(artist==''||artist==undefined)
  773. artist = musicData.artist
  774. } else {
  775. title = file.name
  776. }
  777. }else{
  778. title = file.name
  779. }
  780. }
  781. let name: string = title;
  782. if (!name) {
  783. name = getFileNameWithoutExtension(inputPath);
  784. }
  785. let pixelMap:image.PixelMap|undefined|null = undefined
  786. // Create VideoItem
  787. videoItem = new VideoItem(
  788. name,
  789. inputPath, // id can be generated or left empty
  790. inputPath,
  791. type, // assuming it's local
  792. videoSize,
  793. addTime // convert to ISO string
  794. );
  795. // Set additional properties from format metadata
  796. videoItem.artist = artist;
  797. videoItem.album = album;
  798. videoItem.sampleRate = sampleRate
  799. videoItem.pyStr = pinyin4js.getShortPinyin(name)
  800. videoItem.fileName = FileUtil.getFileName(inputPath);
  801. if(format.duration)
  802. videoItem.duration = formatDuration(format.duration.toString()||'00:00')
  803. videoItem.size = fileSize;
  804. videoItem.bit_rate =formatBitrateToKbps(format.bit_rate || "0");
  805. videoItem.probe_score = format.probe_score;
  806. videoItem.nb_streams = format.nb_streams;
  807. videoItem.nb_programs = format.nb_programs;
  808. videoItem.year = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
  809. videoItem.lyricContent = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
  810. //console.info('readMetaInfoFFmpeg videoItem.lyricContent: ', videoItem.lyricContent);
  811. // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
  812. // tags 必须是 Record<string, string | undefined>(或 any)
  813. // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
  814. if(StrUtil.isEmpty( videoItem.lyricContent)){
  815. const tagsRecord = tags as Record<string, string>;
  816. const possibleKeys = Object.keys(tagsRecord);
  817. for (let i = 0; i < possibleKeys.length; i++) {
  818. const key = possibleKeys[i];
  819. if (key && key.toLowerCase().startsWith('lyrics-')) {
  820. //console.info('readMetaInfoFFmpeg videoItem.key: ', key);
  821. // 通过转换后的 Record 类型安全访问属性
  822. videoItem.lyricContent = tagsRecord[key];
  823. //console.info('readMetaInfoFFmpeg videoItem.lyricContent2: ', videoItem.lyricContent);
  824. if (videoItem.lyricContent) {
  825. break;
  826. }
  827. }
  828. }
  829. }
  830. videoItem.genre = tags.genre||tags.GENRE|| '';
  831. videoItem.track = tags.track||tags.TRACK|| '';
  832. videoItem.ALBUMARTIST = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
  833. videoItem.COMPOSER = tags.COMPOSER||tags.composer|| '';
  834. videoItem.LYRICIST = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
  835. videoItem.COMMENT = tags.COMMENT||tags.comment||tags.COMM|| '';
  836. videoItem.disc = tags.disc|| '';
  837. videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
  838. videoItem.channel_layout = channel_layout || ''
  839. videoItem.channels = channels ||'0'
  840. videoItem.start_time = start_time || '00:00'
  841. videoItem.mimeType = format.format_name
  842. // 检查是否有封面图片流
  843. const hasCover = metadata.streams.some(stream =>
  844. stream.disposition?.attached_pic === 1
  845. );
  846. console.info('readMetaInfoFFmpeg asset hasCover: ', hasCover);
  847. let md5Name = await MD5.digestSync(inputPath)
  848. let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
  849. console.info('readMetaInfoFFmpeg asset imagePath: ', imagePath);
  850. // 如果有封面图片,则提取
  851. try {
  852. if (hasCover) {
  853. //提取封面
  854. let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
  855. if(isSuccess){
  856. imagePath = fileUri.getUriFromPath(imagePath)
  857. }else{
  858. imagePath = ''
  859. }
  860. videoItem.pixelMapPath = imagePath;
  861. }else if(Utility.isVideoByExtension(inputPath)){
  862. pixelMap = await Utility.getFetchFrameByTime(inputPath)
  863. if(pixelMap!==undefined&&pixelMap!==null){
  864. imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,md5Name)
  865. imagePath = fileUri.getUriFromPath(imagePath)
  866. }else{
  867. imagePath = ''
  868. }
  869. imagePath = fileUri.getUriFromPath(imagePath)
  870. videoItem.pixelMapPath = imagePath;
  871. }
  872. } catch (error) {
  873. console.warn(' 提取封面图片失败:', error.message);
  874. }
  875. if(sampleRate&&format.bit_rate){
  876. videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
  877. }
  878. console.info('Successfully parsed metadata:', videoItem);
  879. resolve(videoItem);
  880. })
  881. } catch (error) {
  882. console.error('Failed to parse metadata:', error);
  883. reject(new Error('Failed to parse metadata: ' + error.message));
  884. }
  885. }).catch((error: Error) => {
  886. console.error(`Execution failed with error: ${error.message}`);
  887. reject(error);
  888. });
  889. });
  890. }
  891. private completionNum(num: number): string | number {
  892. if (num < 10) {
  893. return '0' + num;
  894. } else {
  895. return num;
  896. }
  897. }
  898. //根据字节获取大小
  899. static formatFSize(bytes:number):string {
  900. const units = ['Bytes', 'K', 'M', 'G'];
  901. let size = bytes;
  902. let unitIndex = 0;
  903. while (size >= 1024 && unitIndex < units.length - 1) {
  904. size /= 1024;
  905. unitIndex++;
  906. }
  907. // 保留两位小数, 四舍五入
  908. size = Math.round(size * 10) / 10;
  909. return size + units[unitIndex]
  910. }
  911. static gotoMarket(context:common.UIAbilityContext,bundleName:string) {
  912. const want: Want = {
  913. uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}`
  914. };
  915. context.startAbility(want).then(()=>{
  916. //拉起成功
  917. }).catch(()=>{
  918. // 拉起失败
  919. });
  920. }
  921. static async doShare(item:VideoItem,context:common.UIAbilityContext){
  922. // 生成视频封面图
  923. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  924. const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), {
  925. format: 'image/jpeg',
  926. quality: 30
  927. }) as Promise<ArrayBuffer>);
  928. // 构造ShareData,需配置一条有效数据信息
  929. let shareData: systemShare.SharedData = new systemShare.SharedData({
  930. utd: uniformTypeDescriptor.UniformDataType.MEDIA,
  931. uri: fileUri.getUriFromPath(item.filePath),
  932. title: item.name, // 不传title字段时,显示视频文件名
  933. // description: '好听的音乐', // 不传description字段时,显示视频大小
  934. thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
  935. });
  936. // 进行分享面板显示
  937. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  938. // let context = getContext(this) as common.UIAbilityContext;
  939. controller.show(context, {
  940. selectionMode: systemShare.SelectionMode.SINGLE,
  941. previewMode: systemShare.SharePreviewMode.DETAIL,
  942. }).then(() => {
  943. console.info('ShareController show success.');
  944. }).catch((error: BusinessError) => {
  945. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  946. });
  947. }
  948. //分享索引index的视频
  949. static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){
  950. // 构造ShareData,需配置一条有效数据信息
  951. let shareData: systemShare.SharedData = new systemShare.SharedData({
  952. utd: uniformTypeDescriptor.UniformDataType.AUDIO,
  953. uri: fileUri.getUriFromPath(item.filePath),
  954. title: item.name, // 不传title字段时,显示视频文件名
  955. description: '好听的音乐', // 不传description字段时,显示视频大小
  956. });
  957. // 进行分享面板显示
  958. let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
  959. // let context = getContext(this) as common.UIAbilityContext;
  960. controller.show(context, {
  961. selectionMode: systemShare.SelectionMode.SINGLE,
  962. previewMode: systemShare.SharePreviewMode.DETAIL,
  963. }).then(() => {
  964. console.info('ShareController show success.');
  965. }).catch((error: BusinessError) => {
  966. console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
  967. });
  968. }
  969. static getNameList(localList:Array<VideoItem>){
  970. let list: Array<string> = [];
  971. for(let i=0;i<localList.length;i++){
  972. if(StrUtil.isNotEmpty(localList[i].name))
  973. list.push(localList[i].name);
  974. }
  975. return list
  976. }
  977. //获取列表有封面的值
  978. static getFirstCoverFromList(localList:Array<VideoItem>):string{
  979. let list: Array<string> = [];
  980. for(let i=0;i<localList.length;i++){
  981. let pix = localList[i].pixelMapPath
  982. if(pix){
  983. return pix
  984. }
  985. }
  986. return ''
  987. }
  988. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_Dir:私密视频)
  989. static getGlobalNameList(localList:Array<VideoItem>,type:number){
  990. let list: Array<string> = [];
  991. for(let i=0;i<localList.length;i++){
  992. if(localList[i].type === type){
  993. list.push(localList[i].name);
  994. }
  995. }
  996. return list
  997. }
  998. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  999. static getGlobalList(localList:Array<VideoItem>,type:number){
  1000. let list: Array<VideoItem> = [];
  1001. for(let i=0;i<localList.length;i++){
  1002. //播放列表过滤.cue文件
  1003. if(localList[i].type === type&&!localList[i].filePath.toLowerCase().endsWith('.cue') ){
  1004. list.push(localList[i]);
  1005. }
  1006. }
  1007. return list
  1008. }
  1009. //以下是实现 getPlayItem 方法的代码,根据 playType 的值返回随机项或第一项
  1010. static getPlayItem(localList: Array<VideoItem>, playType: number): VideoItem | undefined {
  1011. // 处理空列表情况
  1012. if (!localList || localList.length === 0) {
  1013. return undefined;
  1014. }
  1015. // 当 playType 为 3 时随机返回一个元素
  1016. if (playType === 3) {
  1017. const randomIndex = Math.floor(Math.random() * localList.length);
  1018. return localList[randomIndex];
  1019. }
  1020. // 其他情况返回第一个元素
  1021. else {
  1022. return localList[0];
  1023. }
  1024. }
  1025. /**
  1026. * 根据文件路径从项数组中查找对应的VideoItem
  1027. * @param list VideoItem数组
  1028. * @param filePath 文件路径
  1029. * @returns 对应的VideoItem,如果未找到则返回undefined
  1030. */
  1031. static getItemByFilePath(list: Array<VideoItem>, filePath: string): VideoItem | undefined {
  1032. if (!list || list.length === 0 || !filePath) {
  1033. return undefined;
  1034. }
  1035. for (let i = 0; i < list.length; i++) {
  1036. if (list[i].filePath === filePath) {
  1037. return list[i];
  1038. }
  1039. }
  1040. return undefined;
  1041. }
  1042. //根据filePath获取当前索引index
  1043. static getCurIndexFromGlobalList(localList:Array<VideoItem>,filePath:string){
  1044. let index: number = 0;
  1045. for(let i=0;i<localList.length;i++){
  1046. if(localList[i].filePath === filePath){
  1047. index = i
  1048. return index
  1049. }
  1050. }
  1051. return index
  1052. }
  1053. //新的查询是否收藏的方法
  1054. static getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
  1055. if(ArrayUtil.isEmpty(favList)){
  1056. return false
  1057. }
  1058. if(item===undefined){
  1059. return false
  1060. }
  1061. for(let i=0;i<favList.length;i++){
  1062. if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
  1063. return true
  1064. }
  1065. }
  1066. return false
  1067. }
  1068. static getIsFac(facList: Array<String>,item:VideoItem|undefined){
  1069. if(ArrayUtil.isEmpty(facList)){
  1070. return false
  1071. }
  1072. if(item===undefined){
  1073. return false
  1074. }
  1075. for(let i=0;i<facList.length;i++){
  1076. if(facList[i] === item.name||facList[i]===item.fileName){
  1077. return true
  1078. }
  1079. }
  1080. return false
  1081. }
  1082. static resourceToString(context:Context,resource:Resource){
  1083. if(!context||!resource)
  1084. return ''
  1085. return context.resourceManager.getStringSync(resource).toString()
  1086. }
  1087. //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
  1088. static getGlobalMusicList(localList:Array<VideoItem>,type:number){
  1089. let list: Array<VideoItem> = [];
  1090. for(let i=0;i<localList.length;i++){
  1091. if(localList[i].type === type){
  1092. list.push(localList[i]);
  1093. }
  1094. }
  1095. return list
  1096. }
  1097. static isHaoFreeTime():boolean{
  1098. if(DateUtil.isWeekend()){
  1099. return true
  1100. }
  1101. let str = DateUtil.getFormatDateStr(new Date(), 'HH')
  1102. let a = Number(str);
  1103. if(isNaN(a))
  1104. return false
  1105. // ToastUtil.showToast('a='+a)
  1106. if (a >= 8 && a <13) {//白天
  1107. return false;
  1108. }
  1109. if (a >= 13 && a <20) {//白天
  1110. return false;
  1111. }
  1112. if (a >= 0 && a < 8) {//凌晨
  1113. return true;
  1114. }
  1115. if (a >= 20 && a <= 24) {//晚上
  1116. return true;
  1117. }
  1118. return false
  1119. }
  1120. static getMusisBg():Resource {
  1121. let index = RandomUtil.randomNumber(0,9)
  1122. LogUtil.info('getMusisBg index = '+index)
  1123. return CommonConstants.musicBgList[index]
  1124. }
  1125. static getMusisBg2(index:number):Resource {
  1126. const adjustedIndex = index % 10;
  1127. LogUtil.info('getMusisBg adjustedIndex = '+adjustedIndex)
  1128. return CommonConstants.musicBgList[adjustedIndex]
  1129. }
  1130. static getParentDirectory(filePath: string): string {
  1131. // 使用正则表达式匹配路径分隔符(支持 Unix 和 Windows)
  1132. const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
  1133. if (lastSlashIndex === -1) {
  1134. return '';
  1135. }
  1136. return filePath.substring(0, lastSlashIndex);
  1137. }
  1138. // 开启沉浸式显示模式
  1139. static async enableFullScreen() {
  1140. const ctx = getContext()
  1141. const win = await window.getLastWindow(ctx)
  1142. win.setWindowLayoutFullScreen(true)
  1143. const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
  1144. AppStorage.setOrCreate('topHeight', px2vp(top.topRect.height))
  1145. AppStorage.setOrCreate('bottomHeight', px2vp(top.bottomRect.height))
  1146. }
  1147. // 关闭沉浸式显示模式
  1148. static async disableFullScreen() {
  1149. const ctx = getContext()
  1150. const win = await window.getLastWindow(ctx)
  1151. win.setWindowLayoutFullScreen(false)
  1152. AppStorage.setOrCreate('topHeight', 0)
  1153. AppStorage.setOrCreate('bottomHeight', 0)
  1154. }
  1155. // 设置状态栏文字颜色为白色
  1156. static async setStatusBarLight() {
  1157. const ctx = getContext()
  1158. const win = await window.getLastWindow(ctx)
  1159. win.setWindowSystemBarProperties({
  1160. statusBarContentColor: '#ffffff'
  1161. })
  1162. }
  1163. // 设置状态栏文字颜色为黑色
  1164. static async setStatusBarDark() {
  1165. const ctx = getContext()
  1166. const win = await window.getLastWindow(ctx)
  1167. win.setWindowSystemBarProperties({
  1168. statusBarContentColor: '#000000'
  1169. })
  1170. }
  1171. // 优化点:完全基于Promise链的异步处理
  1172. static async getAppName(context: Context): Promise<string> {
  1173. try {
  1174. // 1. 同步化BundleInfo获取
  1175. const bundleInfo = await bundleManager.getBundleInfoForSelf(
  1176. bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  1177. );
  1178. // 2. 安全获取labelId(新增空值校验)
  1179. const labelId = bundleInfo.appInfo?.labelId;
  1180. if (!labelId || labelId <= 0) {
  1181. throw new Error("Invalid labelId: " + labelId);
  1182. }
  1183. // 3. 异步资源解析(替换危险的getStringSync)
  1184. return await context.resourceManager.getStringValue(labelId);
  1185. } catch (err) {
  1186. console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`);
  1187. // 4. 多级降级策略
  1188. return AppUtil.getBundleName() // 最终兜底
  1189. }
  1190. }
  1191. //按名称升序
  1192. static doSortListAscending( list:Array<VideoItem>,isShowFileName?:boolean){
  1193. let options: Intl.CollatorOptions = {
  1194. localeMatcher: "lookup",
  1195. usage: "sort",
  1196. sensitivity: "accent", // 对中文更友好
  1197. numeric:true
  1198. };
  1199. const collator = new Intl.Collator("zh-CN", options);
  1200. list.sort((a, b) => {
  1201. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1202. if (typeOrder !== 0) {
  1203. return typeOrder;
  1204. }
  1205. if(isShowFileName){
  1206. return collator.compare(a.fileName||a.name, b.fileName||b.name)
  1207. }else{
  1208. return collator.compare(a.name, b.name)
  1209. }
  1210. });
  1211. }
  1212. //按名称降序
  1213. static doSortListDescending(list:Array<VideoItem>,isShowFileName?:boolean){
  1214. const options: Intl.CollatorOptions = {
  1215. localeMatcher: "lookup",
  1216. usage: "sort",
  1217. sensitivity: "accent", // 对中文更友好
  1218. numeric:true
  1219. };
  1220. const collator = new Intl.Collator("zh-CN", options);
  1221. list.sort((a, b) => {
  1222. const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
  1223. if (typeOrder !== 0) {
  1224. return typeOrder;
  1225. }
  1226. if(isShowFileName){
  1227. return collator.compare(b.fileName||b.name, a.fileName||a.name)
  1228. }else{
  1229. return collator.compare(b.name, a.name)
  1230. }
  1231. // const partsA = extractParts(a.name);
  1232. // const partsB = extractParts(b.name);
  1233. //
  1234. // const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric);
  1235. // if (nonNumericComparison !== 0) {
  1236. // return nonNumericComparison;
  1237. // }
  1238. // return partsB.numeric - partsA.numeric;
  1239. });
  1240. }
  1241. /**
  1242. * 读取本地会员到期时间(解密.vv文件)
  1243. * @returns
  1244. */
  1245. static async getLocalNobleExpireDate(): Promise<string> {
  1246. // return "2025-08-08 12:00";
  1247. try {
  1248. const documentViewPicker = new picker.DocumentViewPicker()
  1249. let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
  1250. let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/'
  1251. let filePath = download_path + STR_LOCK_VIDEO+ '/' + VIP_FILEPATH
  1252. let expireDate = Utility.readDataFromFile(filePath)
  1253. return expireDate;
  1254. } catch (e) {
  1255. return '';
  1256. }
  1257. }
  1258. static readDataFromFile(filePath: string) {
  1259. if(!FileUtil.accessSync(filePath)){
  1260. return ''
  1261. }
  1262. let readTextOptions: ReadTextOptions = {
  1263. offset: 0,
  1264. length: 0,
  1265. encoding: 'utf-8'
  1266. };
  1267. let stat = fileIo.statSync(filePath);
  1268. readTextOptions.length = stat.size;
  1269. let str = fileIo.readTextSync(filePath, readTextOptions);
  1270. const parsedData:VipData = JSON.parse(str) as VipData;
  1271. // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate)
  1272. let result = Base64Util.decodeSync( parsedData.expireDate)
  1273. // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result))
  1274. return StrUtil.unit8ArrayToStr(result);
  1275. }
  1276. }
  1277. interface PathStat {
  1278. path: string;
  1279. isDirectory: boolean;
  1280. }
  1281. async function isDirectory(filePath: string): Promise<boolean> {
  1282. try {
  1283. const stat = await fs.stat(filePath);
  1284. return stat.isDirectory();
  1285. } catch (error) {
  1286. console.error('Error getting file stat:', error);
  1287. return false;
  1288. }
  1289. }
  1290. //排序模式参数
  1291. interface VideoNameParts {
  1292. nonNumeric: string;
  1293. numeric: number;
  1294. suffix:string;
  1295. }
  1296. function extractParts(name: string): VideoNameParts {
  1297. // 更完善的正则表达式,处理各种情况
  1298. // 匹配:非数字前缀 + 数字部分 + 可选的剩余部分
  1299. const match = name.match(/^(\D*)(\d+)(.*)$/);
  1300. if (match) {
  1301. return {
  1302. nonNumeric: match[1] || '', // 非数字前缀
  1303. numeric: parseInt(match[2], 10), // 数字部分
  1304. suffix: match[3] || '' // 剩余部分(可能包含扩展名等)
  1305. };
  1306. }
  1307. // 如果没有找到数字模式,将整个名称作为非数字部分
  1308. return {
  1309. nonNumeric: name,
  1310. numeric: 0,
  1311. suffix: ''
  1312. };
  1313. }
  1314. function getInstallTime(): number | null {
  1315. // 从本地存储中获取安装时间
  1316. return PreferencesUtil.getNumberSync('installTime')
  1317. }
  1318. function setInstallTime(time: number) {
  1319. // 将安装时间存储到本地存储中
  1320. PreferencesUtil.putSync('installTime',time)
  1321. }
  1322. function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise<image.PixelMap | null> {
  1323. return new Promise((resolve, reject) => {
  1324. avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => {
  1325. if (error) {
  1326. console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`);
  1327. resolve(null);
  1328. } else {
  1329. resolve(pixelMap);
  1330. }
  1331. avMetadataExtractor.release();
  1332. });
  1333. });
  1334. }
  1335. //排序类型
  1336. function getTypeOrder(type: number) {
  1337. switch (type) {
  1338. case CommonConstants.TYPE_IS_DIR:
  1339. return 1; // First
  1340. case CommonConstants.TYPE_IS_CSJAD:
  1341. return 2; // Middle
  1342. case CommonConstants.TYPE_LOCAL:
  1343. return 3; // Last
  1344. default:
  1345. return 4; // Unknown types, if any, go last
  1346. }
  1347. }
  1348. function convertSecondsToTime(secondsStr: string): string {
  1349. if (!secondsStr || isNaN(Number(secondsStr))) {
  1350. return "00:00";
  1351. }
  1352. let seconds = parseInt(secondsStr, 10);
  1353. seconds = Math.floor(seconds / 1000);
  1354. const hours = Math.floor(seconds / 3600);
  1355. const minutes = Math.floor((seconds % 3600) / 60);
  1356. const secs = seconds % 60;
  1357. const formatNumber = (num: number) => num.toString().padStart(2, '0');
  1358. if (hours > 0) {
  1359. return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`;
  1360. } else {
  1361. return `${formatNumber(minutes)}:${formatNumber(secs)}`;
  1362. }
  1363. }
  1364. /**
  1365. * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS)
  1366. * @param seconds 秒数字符串(如 "283.524351" 或 "3675")
  1367. * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断)
  1368. * @returns 格式化后的时间字符串
  1369. */
  1370. function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
  1371. // 1. 校验输入
  1372. const secNum = parseFloat(seconds);
  1373. if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00";
  1374. // 2. 计算时间分量
  1375. const totalSec = Math.floor(secNum);
  1376. const hours = Math.floor(totalSec / 3600);
  1377. const mins = Math.floor((totalSec % 3600) / 60);
  1378. const secs = totalSec % 60;
  1379. // 3. 格式化输出
  1380. const pad = (n: number) => n.toString().padStart(2, '0');
  1381. return forceHHMMSS || hours > 0
  1382. ? `${pad(hours)}:${pad(mins)}:${pad(secs)}` // HH:MM:SS
  1383. : `${pad(mins)}:${pad(secs)}`; // MM:SS
  1384. }
  1385. /**
  1386. * 定义解析结果的数据结构
  1387. */
  1388. class MusicInfo {
  1389. artist: string = ""; // 艺术家名称
  1390. title: string = ""; // 歌曲名称
  1391. isValid: boolean = false; // 格式是否有效
  1392. }
  1393. /**
  1394. * 精简版常见中文姓氏(音乐场景优化版)
  1395. */
  1396. const COMMON_CHINESE_SURNAMES = [
  1397. // 超常见姓氏(覆盖约80%人口)
  1398. '李','王','张','刘','陈','杨','黄','赵','吴','周',
  1399. '徐','孙','马','朱','胡','林','郭','何','高','罗',
  1400. '郑','梁','谢','宋','唐','许','韩','邓','曹','彭',
  1401. '曾','萧','田','董','潘','袁','于','蔡','余','杜',
  1402. // 音乐行业常见姓氏(歌手高频姓)
  1403. '汪','苏','薛','谭','华','金','魏','陶','姜','窦',
  1404. '章','毛','易','方','宋','任','沈','贾','江','孔',
  1405. // 组合/乐队常见字
  1406. '羽','泉','信','乐','花','飞','龙','风','云','草'
  1407. ];
  1408. /**
  1409. * 歌手特征关键词(优化版)
  1410. */
  1411. const ARTIST_KEYWORDS = [
  1412. // 中文特征
  1413. '乐队','组合','乐团','和','&','合唱',' featuring',
  1414. // 英文特征
  1415. 'feat','ft','with','vs','presents','presents',
  1416. // 符号特征
  1417. '×','X','※','◆','♫'
  1418. ];
  1419. /**
  1420. * 智能解析音乐文件名(支持多种分隔符和格式)
  1421. * @param fileName - 待解析的完整文件名
  1422. * @returns 结构化音乐信息
  1423. */
  1424. /**
  1425. * 智能解析音乐文件名
  1426. */
  1427. function parseMusicFileName(fileName: string): MusicInfo {
  1428. const result: MusicInfo = new MusicInfo();
  1429. if (!fileName) return result;
  1430. // 预处理
  1431. const cleanName: string = fileName.trim();
  1432. const lastDotIndex: number = cleanName.lastIndexOf('.');
  1433. const baseName: string = lastDotIndex > 0 ?
  1434. cleanName.substring(0, lastDotIndex).trim() :
  1435. cleanName;
  1436. // 支持的分隔符(明确定义类型)
  1437. const separators: string[] = ['-', '-', '—', '~', '~'];
  1438. // 寻找分隔位置
  1439. let bestSplitIndex: number = -1;
  1440. for (const sep of separators) {
  1441. const index: number = baseName.lastIndexOf(sep);
  1442. if (index > bestSplitIndex) {
  1443. bestSplitIndex = index;
  1444. }
  1445. }
  1446. // 分割字符串
  1447. if (bestSplitIndex > 0 && bestSplitIndex < baseName.length - 1) {
  1448. let part1: string = baseName.substring(0, bestSplitIndex).trim();
  1449. let part2: string = baseName.substring(bestSplitIndex + 1).trim();
  1450. // 处理前缀序号
  1451. part1 = part1.replace(/^\d+[\s\.\-- —~~]*/, '').trim();
  1452. // 判断歌手部分(明确定义返回类型)
  1453. const identifyArtist = (str: string): boolean => {
  1454. return COMMON_CHINESE_SURNAMES.some((surname: string) =>
  1455. str.startsWith(surname) ||
  1456. new RegExp(`[ ,,、&&]${surname}`).test(str)
  1457. ) || ARTIST_KEYWORDS.some((keyword: string) =>
  1458. str.includes(keyword)
  1459. );
  1460. };
  1461. // 判断歌手位置
  1462. const part1IsArtist: boolean = identifyArtist(part1);
  1463. const part2IsArtist: boolean = identifyArtist(part2);
  1464. if (part1IsArtist && !part2IsArtist) {
  1465. result.artist = part1;
  1466. result.title = part2;
  1467. } else if (part2IsArtist && !part1IsArtist) {
  1468. result.artist = part2;
  1469. result.title = part1;
  1470. } else {
  1471. result.artist = part1.length <= part2.length ? part1 : part2;
  1472. result.title = part1.length <= part2.length ? part2 : part1;
  1473. }
  1474. // 后处理
  1475. result.title = result.title
  1476. .replace(/(?:\(|()[^))]*(?:)|\))/g, '')
  1477. .replace(/\s*[—-]\s*(?:Live|Version|Remix|伴奏).*/i, '')
  1478. .trim();
  1479. // 有效性验证
  1480. result.isValid = result.artist.length > 0 &&
  1481. result.title.length > 0;
  1482. } else {
  1483. result.title = baseName;
  1484. result.isValid = result.title.length > 0;
  1485. }
  1486. return result;
  1487. }
  1488. function getFileNameWithoutExtension(filePath: string): string {
  1489. const fileName = filePath.split('/').pop() || '';
  1490. const lastDotIndex = fileName.lastIndexOf('.');
  1491. return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
  1492. }
  1493. /**
  1494. * 从音乐文件中提取封面
  1495. * @param inputPath 音乐文件路径
  1496. * @returns Promise<void>
  1497. */
  1498. // async function getFFmpegCover(inputPath: string, outputPath: string) : Promise<boolean>{
  1499. // let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
  1500. // FFmpeg.execute(commands, {
  1501. // logCallback: (logLevel: number, logMessage: string) => {
  1502. // console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`)
  1503. // },
  1504. // progressCallback: (message: string) => {
  1505. // console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
  1506. // },
  1507. // })
  1508. // .then(() => {
  1509. // return true
  1510. // console.info("FFmpeg execution succeeded.");
  1511. // })
  1512. // .catch((error: Error) => {
  1513. // return false
  1514. // console.error(`FFmpeg execution failed with error: ${error.message}`);
  1515. // });
  1516. // return ;
  1517. // }
  1518. /**
  1519. * 从音乐文件中提取封面
  1520. * @param inputPath 音乐文件路径
  1521. * @param outputPath 封面输出路径
  1522. * @returns Promise<boolean> 表示操作是否成功
  1523. */
  1524. async function getFFmpegCover(inputPath: string, outputPath: string): Promise<boolean> {
  1525. let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
  1526. try {
  1527. await FFmpeg.execute(commands, {
  1528. logCallback: (logLevel: number, logMessage: string) => {
  1529. console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
  1530. },
  1531. progressCallback: (message: string) => {
  1532. console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
  1533. },
  1534. });
  1535. console.info("FFmpeg execution succeeded.");
  1536. return true;
  1537. } catch (error) {
  1538. console.error(`FFmpeg execution failed with error: ${error instanceof Error ? error.message : String(error)}`);
  1539. return false;
  1540. }
  1541. }
  1542. /**
  1543. * 从视频文件中提取封面 提前视频第5帧的封面
  1544. * @param inputPath 文件路径
  1545. * @returns Promise<void>
  1546. */
  1547. async function getVideoFFmpegCover(inputPath: string, outputPath: string) {
  1548. // 更可靠的命令,专门提取视频第一帧作为封面
  1549. const commands = [
  1550. "ffmpeg",
  1551. "-y", // 覆盖输出文件(如果存在)
  1552. "-i", inputPath, // 输入文件
  1553. "-ss", "00:00:00", // 定位到开始位置
  1554. "-vframes", "1", // 只提取1帧
  1555. "-q:v", "2", // 高质量JPEG(1-31,2是最高质量)
  1556. "-f", "image2", // 强制输出为图像格式
  1557. outputPath
  1558. ];
  1559. FFmpeg.execute(commands, {
  1560. logCallback: (logLevel: number, logMessage: string) => {
  1561. console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`)
  1562. },
  1563. progressCallback: (message: string) => {
  1564. console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
  1565. },
  1566. })
  1567. .then(() => {
  1568. })
  1569. .catch((error: Error) => {
  1570. console.error(`FFmpegX execution failed with error: ${error.message}`);
  1571. });
  1572. }
  1573. /**
  1574. * 从音乐文件中提取歌词内容
  1575. * @param inputPath 音乐文件路径
  1576. * @returns Promise<string> 直接返回歌词内容,若无歌词则返回空字符串
  1577. */
  1578. async function extractLyricsContent(inputPath: string): Promise<string> {
  1579. return new Promise(async (resolve, reject) => {
  1580. try {
  1581. // 1. 使用ffprobe获取元数据
  1582. const commands = [
  1583. 'ffprobe',
  1584. '-v', 'quiet',
  1585. '-print_format', 'json',
  1586. '-show_format',
  1587. inputPath
  1588. ];
  1589. let outputJson = '';
  1590. await FFmpeg.execute(commands, {
  1591. outputCallback: (message: string) => outputJson += message,
  1592. });
  1593. // 2. 解析歌词标签
  1594. const metadata:FFprobeMetadata = JSON.parse(outputJson);
  1595. const tags = metadata.format?.tags || {};
  1596. // 3. 从常见标签中查找歌词(优先级顺序)
  1597. const lyricContent =
  1598. tags.LYRICS || // 标准标签
  1599. tags.lyrics || // 小写变体
  1600. tags.USLT || // ID3v2标签
  1601. tags.UNSYNCEDLYRICS ||
  1602. '';
  1603. resolve(lyricContent.trim());
  1604. } catch (error) {
  1605. reject(`解析失败: ${error instanceof Error ? error.message : String(error)}`);
  1606. }
  1607. });
  1608. }
  1609. /**
  1610. * 解析音乐文件元数据(包含歌词和其他关键字段)
  1611. * @param inputPath 文件路径
  1612. * @returns Promise<VideoItem> 包含完整元数据的对象
  1613. */
  1614. async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
  1615. return new Promise(async (resolve, reject) => {
  1616. try {
  1617. // 1. 执行FFprobe命令
  1618. const commands: string[] = [
  1619. 'ffprobe',
  1620. '-v', 'quiet',
  1621. '-print_format', 'json',
  1622. '-show_format',
  1623. '-show_streams',
  1624. inputPath
  1625. ];
  1626. let outputJson = '';
  1627. await FFmpeg.execute(commands, {
  1628. outputCallback: (message: string) => outputJson += message,
  1629. });
  1630. // 2. 解析元数据
  1631. const metadata = JSON.parse(outputJson) as FFprobeMetadata;
  1632. const format:FFprobeFormat = metadata.format ;
  1633. const tags = format.tags||{} ;
  1634. // 3. 构建VideoItem基础信息
  1635. const videoItem = new VideoItem(
  1636. tags.title || getFileNameWithoutExtension(inputPath),
  1637. inputPath, // id
  1638. inputPath,
  1639. CommonConstants.TYPE_LOCAL,
  1640. 0,
  1641. new Date().toISOString() // 使用当前时间作为默认创建时间
  1642. );
  1643. // 4. 设置关键字段
  1644. videoItem.bit_rate = format.bit_rate || '';
  1645. videoItem.probe_score = format.probe_score || 0;
  1646. videoItem.year = tags.TYER || tags.date || '';
  1647. videoItem.lyricContent =
  1648. tags.LYRICS ??
  1649. tags.lyrics ??
  1650. tags.USLT ??
  1651. tags.UNSYNCEDLYRICS ??
  1652. '';
  1653. // 5. 设置其他可选字段
  1654. videoItem.artist = tags.artist || '';
  1655. videoItem.album = tags.album || '';
  1656. videoItem.duration = format.duration || '';
  1657. videoItem.nb_streams = format.nb_streams;
  1658. videoItem.nb_programs = format.nb_programs;
  1659. resolve(videoItem);
  1660. } catch (error) {
  1661. reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message : String(error)}`));
  1662. }
  1663. });
  1664. }
  1665. /**
  1666. * 将比特率(bps)转换为 kbps 并格式化
  1667. * @param bitRate 比特率字符串(如 "5644802")
  1668. * @param decimalPlaces 保留小数位数(默认0)
  1669. * @returns 格式化后的 kbps 字符串(如 "5644 kbps")
  1670. */
  1671. function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string {
  1672. // 1. 转换为数字
  1673. const bitsPerSecond = parseInt(bitRate);
  1674. if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps";
  1675. // 2. 计算 kbps(1 kbps = 1000 bps)
  1676. const kbps = bitsPerSecond / 1000;
  1677. // 3. 格式化输出
  1678. return `${kbps.toFixed(decimalPlaces)} kbps`;
  1679. }
  1680. // 定义音质等级
  1681. export enum AudioQuality {
  1682. LQ = "LQ", // 低音质
  1683. HQ = "HQ", // 高音质
  1684. SQ = "SQ", // 无损
  1685. HIRES = "Hi-Res", // 高解析度
  1686. HR = "HR" // 高解析度
  1687. }
  1688. // 明确定义支持的音频格式类型
  1689. interface SupportedFormats {
  1690. LOSSY: Set<string>;
  1691. LOSSLESS: Set<string>;
  1692. }
  1693. // 声明并初始化支持的音频格式(符合类型定义)
  1694. const SUPPORTED_FORMATS: SupportedFormats = {
  1695. LOSSY: new Set(['mp3', 'aac', 'ogg', 'opus', 'wma']),
  1696. LOSSLESS: new Set(['flac', 'alac', 'ape', 'wav', 'aiff', 'dsf','aif'])
  1697. };
  1698. /**
  1699. * 根据音频参数判断音质等级
  1700. * @param format 编码格式(字符串,不区分大小写,如"MP3"或"flac")
  1701. * @param bitrate 比特率(单位:bps,如320kbps需传入320000)
  1702. * @param sampleRate 采样率(单位:Hz,如44.1kHz需传入44100)
  1703. * @param bitDepth 位深(单位:bit,默认16bit,Hi-Res需≥24bit)
  1704. * @returns AudioQuality 音质等级
  1705. */
  1706. function determineAudioQuality(
  1707. format: string,
  1708. bitrate: number,
  1709. sampleRate: number,
  1710. ): AudioQuality {
  1711. // 统一转为小写便于比较
  1712. const normalizedFormat = format.toLowerCase();
  1713. // --------------- 第一步:判断是否无损/Hi-Res ---------------
  1714. const isLossless = SUPPORTED_FORMATS.LOSSLESS.has(normalizedFormat);
  1715. if (isLossless) {
  1716. // Hi-Res标准:采样率≥96kHz且位深≥24bit
  1717. if (sampleRate >= 48000 ) {
  1718. return AudioQuality.HIRES;
  1719. }
  1720. // CD级无损标准:采样率≥44.1kHz且位深≥16bit
  1721. if (sampleRate >= 44100) {
  1722. return AudioQuality.SQ;
  1723. }
  1724. }
  1725. // --------------- 第二步:有损音质判断 ---------------
  1726. // 高音质标准:比特率≥256kbps且采样率≥44.1kHz
  1727. if (bitrate >= 256000 && sampleRate >= 44100) {
  1728. return AudioQuality.HQ;
  1729. }
  1730. // 其余情况为低音质
  1731. else {
  1732. return AudioQuality.LQ;
  1733. }
  1734. }
  1735. function getFileFormatByPath(filePath: string): string {
  1736. // 检查文件路径是否有效
  1737. if (!filePath || filePath.length === 0) {
  1738. console.error(' 文件路径不能为空');
  1739. return '';
  1740. }
  1741. // 获取最后一个点号的位置
  1742. const lastDotIndex = filePath.lastIndexOf('.');
  1743. // 如果没有点号或者点号在最后一位,返回空字符串
  1744. if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) {
  1745. return '';
  1746. }
  1747. // 返回点号后的部分(转换为小写)
  1748. return filePath.substring(lastDotIndex + 1).toLowerCase();
  1749. }
  1750. // 检查乱码的简单方法
  1751. function hasGarbledText(tags: FFMpegTags): boolean {
  1752. if (!tags) return false;
  1753. const text = [tags.title, tags.artist, tags.album].join("");
  1754. return (
  1755. /[À-ÿµØÇò]/.test(text) || // 拉丁乱码
  1756. /[\uFFFD�]/.test(text) || // Unicode替换字符
  1757. /[�□▪▫▢▣▤▥▦▧▨▩▪▫◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯]/.test(text) || // 几何图形
  1758. /[\uD800-\uDFFF]/.test(text) // 无效的UTF-16代理对
  1759. );
  1760. }
  1761. // function hasGarbledText(tags: FFMpegTags): boolean {
  1762. // if (!tags) return false;
  1763. // const text = [tags.title, tags.artist, tags.album].join("");
  1764. // return /[À-ÿµØÇò]/.test(text); // 匹配常见乱码字符
  1765. // }
  1766. /**
  1767. * 提取华为接口的媒体文件元数据
  1768. * @param filePath 文件路径(如:/data/storage/el1/base/test.mp3 )
  1769. * @returns Promise<FFMpegTags> 包含专辑、艺术家、标题的元数据对象
  1770. */
  1771. export async function extractHwMediaMetadata(filePath: string): Promise<FFMpegTags> {
  1772. const tags: FFMpegTags = {};
  1773. if (!canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
  1774. console.warn("Metadata extraction not supported on this device");
  1775. return tags;
  1776. }
  1777. try {
  1778. const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
  1779. const fd = await fs.open(filePath, fs.OpenMode.READ_ONLY);
  1780. // 设置文件描述符并提取元数据
  1781. avMetadataExtractor.fdSrc = fd;
  1782. const metadata = await avMetadataExtractor.fetchMetadata();
  1783. // 映射标准FFMpeg标签
  1784. if (metadata) {
  1785. tags.album = metadata.album ?? undefined;
  1786. tags.artist = metadata.artist ?? metadata.author ?? undefined; // 兼容不同元数据字段
  1787. tags.title = metadata.title ?? undefined;
  1788. }
  1789. // 关闭文件描述符
  1790. await fs.close(fd);
  1791. return tags;
  1792. } catch (error) {
  1793. const err = error as BusinessError;
  1794. console.error(`Metadata extraction failed: ${err.code}, ${err.message}`);
  1795. return tags;
  1796. }
  1797. return tags;
  1798. }