| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283 |
- import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
- import {
- AppUtil,
- ArrayUtil,
- Base64Util,
- DateUtil, FileUtil, ImageUtil, LogUtil,
- MD5,
- PreferencesUtil,
- RandomUtil, StrUtil } from '@pura/harmony-utils';
- import { media } from '@kit.MediaKit';
- import fs, { ReadTextOptions } from '@ohos.file.fs';
- import { image } from '@kit.ImageKit';
- import fileIo from '@ohos.file.fs';
- import { VideoItem } from '../../viewmodel/VideoItem';
- import { photoAccessHelper } from '@kit.MediaLibraryKit';
- import { dataSharePredicates, uniformTypeDescriptor } from '@kit.ArkData';
- import { systemShare } from '@kit.ShareKit';
- import { fileUri, picker } from '@kit.CoreFileKit';
- import { common, UIAbility, Want } from '@kit.AbilityKit';
- import { CommonConstants, STR_LOCK_VIDEO, VIP_FILEPATH } from '../constants/CommonConstants';
- import { window } from '@kit.ArkUI';
- import { bundleManager } from '@kit.AbilityKit'
- import { pinyin4js } from '@ohos/pinyin4js';
- import { VipData } from '../../viewmodel/VipData';
- import { VipPage } from '../../pages/VipPage';
- import { FFmpeg, FFProgressMessageParser } from '@sj/ffmpeg';
- import ReqPermissionUtil from './ReqPermissionUtil';
- import { commentManager } from '@kit.AppGalleryKit';
- import { hilog } from '@kit.PerformanceAnalysisKit';
- import Logger from './Logger';
- export interface FFMpegTags {
- album?: string;
- ALBUM?: string;
- artist?: string;
- ARTIST?: string;
- TITLE?: string;
- title?: string;
- track?: string;
- TRACK?: string;
- TYER?: string;
- year?: string;
- genre?:string;
- GENRE?:string;
- date?: string;
- DATE?: string;
- LYRICS?: string;
- lyrics?: string; // 小写变体
- USLT?: string; // ID3v2同步歌词
- UNSYNCEDLYRICS?: string; // ID3v2非同步歌词
- // Add any other tag properties you expect
- ALBUMARTIST?:string//专辑艺术家
- COMPOSER?:string//作曲家
- LYRICIST?:string//作词家
- COMMENT?:string//注释
- albumartist?:string//专辑艺术家
- album_artist?:string//专辑艺术家
- TPE2?:string//专辑艺术家
- composer?:string//作曲家
- lyricist?:string//作词家
- TEXT?:string//作词家
- comment?:string//注释
- COMM?:string//注释
- comm?:string//注释
- disc?:string//音碟号
- }
- interface FFprobeFormat {
- filename: string;
- nb_streams: number;
- nb_programs: number;
- format_name: string;
- format_long_name: string;
- duration: string;
- size: string;
- bit_rate: string;
- probe_score: number;
- tags?: FFMpegTags;
- }
- interface FFprobeStream {
- // Define stream properties as needed
- codec_name?: string;
- sample_fmt?: string;
- codec_type?: string; // 流类型,如"audio"、"video"
- sample_rate?: string; // 采样率
- bit_rate?: string; // 比特率
- disposition?: StreamDisposition; // 添加disposition属性
- bits_per_raw_sample?: string; // 位深
- channels?: string; // 声道数
- channel_layout?: string; // 声道布局
- start_time?: string; // 起始播放
- }
- interface StreamDisposition {
- default?: number;
- dub?: number;
- original?: number;
- comment?: number;
- lyrics?: number;
- karaoke?: number;
- forced?: number;
- hearing_impaired?: number;
- visual_impaired?: number;
- clean_effects?: number;
- attached_pic?: number; // 添加封面图片标识
- timed_thumbnails?: number;
- captions?: number;
- descriptions?: number;
- metadata?: number;
- dependent?: number;
- still_image?: number;
- }
- export interface FFprobeMetadata {
- streams: FFprobeStream[];
- format: FFprobeFormat;
- }
- export class Utility {
- private constructor() {}
- static isOpenTime():boolean{
- const currentDate = new Date();
- const targetDate = new Date(CommonConstants.OPEN_DATE);
- if (currentDate > targetDate) {
- return true
- }
- return false
- }
- static readonly FOREVER_DATE: string = 'forever';
- /**
- * 是否会员(只看线上会员状态)
- */
- static isNoble(): boolean {
- // 只判断线上会员状态
- return PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
- }
- static isForever(): boolean {
- // 判断是不是永久会员
- return PreferencesUtil.getBooleanSync('isForever', false);
- }
- /**
- * 旧版客户端判断会员状态
- * @returns
- */
- static isNobleForOld():boolean{
- if(!Utility.isOpenTime())//提交审核的审核,刚刚开始可以全部是赞助用户。
- return true
- return PreferencesUtil.getBooleanSync('isNoble',false)
- }
- /**
- * setNoble方法废弃,保留兼容但不做任何操作
- */
- static setNoble(expireDate:string){
- let isNoble = false
- if(StrUtil.isNotEmpty(expireDate)){
- if(expireDate===VipPage.FOREVER_DATE){
- isNoble = true
- }else{
- const currentDate = new Date();
- const targetDate = DateUtil.getFormatDate(expireDate)
- if (currentDate > targetDate) {
- isNoble = false
- }else{
- isNoble = true
- }
- }
- }
- PreferencesUtil.putSync('isNoble',isNoble)
- }
- /**
- * 判断用户安装app是否超过2天,超过2天(2800分钟)才展示广告 显示广告 24*60=1440 1440*2=2800
- * @param day
- * @returns
- */
- static isPassInstallTime(day:number):boolean{
- //获取当前时间
- let currentTime = new Date().getTime();
- // 尝试从存储中获取安装时间
- let installTime = getInstallTime();
- if (installTime===0||installTime === null|| installTime===undefined) {
- // 如果没有存储安装时间,则存储当前时间为安装时间
- setInstallTime(currentTime);
- return false
- } else {
- // 计算时间差(以分钟为单位)
- const timeDifference = (currentTime - installTime) / (1000 * 60);
- LogUtil.debug("onecold timeDifference=" + timeDifference)
- // 如果超过5分钟,显示广告 24*60=1440 1440*2=2800
- if (timeDifference > 24*60*day) {
- return true
- }else{
- return false
- }
- }
- }
- // 定义一个函数来获取当前的时间段
- static getTimePeriod(): string {
- const now = new Date();
- const hours = now.getHours();
- if (hours >= 5 && hours < 12) {
- return "早上好";
- } else if (hours >= 12 && hours < 18) {
- return "下午好";
- } else if (hours >= 18 && hours < 22) {
- return "晚上好";
- } else {
- return "夜深了";
- }
- }
- static optimizedFormat(speed: number): string {
- if (speed) {
- const str = speed.toFixed(2);
- let end = str.length;
- while (end > 0 && (str[end - 1] === '0' || str[end - 1] === '.')) {
- end--;
- if (str[end] === '.') {
- break;
- }
- }
- return str.slice(0, end || 1) + 'x';
- } else {
- return '1x'
- }
- }
- static isHaoOpenTime():boolean{
- const currentDate = new Date();
- const targetDate = new Date('2025-04-20');
- if (currentDate < targetDate) {
- return true
- }
- return false
- }
- static addDays(date: Date, days: number): Date {
- const result = new Date(date);
- result.setDate(result.getDate() + days);
- return result;
- }
- static convertToKHz(sampleRateHz: string|undefined): string {
- if(StrUtil.isEmpty(sampleRateHz)||sampleRateHz ===undefined)
- return '0KHz'
- const sampleRateKHz = Number(sampleRateHz) / 1000;
- return `${sampleRateKHz} KHz`;
- }
- static showHaoPingDialog(uicontext: UIContext,context:common.UIAbilityContext,appName:string,bundleName:string) {
- try {
- uicontext.getPromptAction().showDialog({
- title: `喜欢${appName}吗?`,
- message:'如果这个应用对您有帮助,请给我们一个五星好评,谢谢!让我们一起为鸿蒙生态助力,您的支持是我们努力提升的动力!让我们一起共赴鸿蒙满天星光。',
- buttons: [
- {
- text: '下次再说',
- color: '#000000'
- },
- {
- text: '给个好评',
- color: '#000000'
- },
- ]
- }, (err, data) => {
- if (err) {
- console.error('showDialog err: ' + err);
- return;
- }
- // 根据点击的按钮索引处理不同逻辑
- if (data.index === 1) {//查看路径
- Utility.gotoMarket(context, bundleName)
- }
- console.info('showDialog success callback, click button: ' + data.index);
- });
- } catch (error) {
- let message = (error as BusinessError).message;
- let code = (error as BusinessError).code;
- console.error(`showDialog args error code is ${code}, message is ${message}`);
- };
- }
- static isMerge(localList:Array<VideoItem>,packName:string):boolean{
- if(ArrayUtil.isEmpty(localList))
- return false
- for(let i=0;i<localList.length;i++){
- if(!localList[i].filePath.includes(packName)){
- return false
- }
- }
- return true
- }
- /**
- * 格式化媒体格式类型显示
- * @param mimeType 媒体格式类型字符串
- * @return 格式化后的显示字符串
- */
- static formatMimeType(mimeType: string | undefined): string {
- if (StrUtil.isEmpty(mimeType) || mimeType === undefined) {
- return 'unknown';
- }
- // 从MIME类型中提取格式部分,例如 "audio/mp3" -> "MP3"
- try {
- const parts = mimeType.split('/');
- if (parts.length > 1) {
- return parts[1].toUpperCase();
- } else {
- return mimeType.toUpperCase();
- }
- } catch (err) {
- console.error(`格式化媒体类型出错: ${err}`);
- return 'unknown';
- }
- }
- //根据字节获取大小
- static formatFileSize(bytes:number) {
- const units = ['Bytes', 'Kbps', 'Mbps'];
- let size = bytes;
- let unitIndex = 0;
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
- if(size > 1024&&unitIndex==2){
- size = size*0.1
- }
- if(size > 1024&&unitIndex==2){
- size = size*0.1
- }
- if(size > 480&&unitIndex==2){
- size = size*0.5
- }
- // 保留两位小数, 四舍五入
- size = Math.round(size * 10) / 10;
- return size + units[unitIndex]
- }
- //根据字节获取大小无单位
- static formatFileSizeWithout(bytes:number) {
- const units = ['Bytes', 'Kbps', 'Mbps'];
- let size = bytes;
- let unitIndex = 0;
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
- if(size > 1024&&unitIndex==2){
- size = size*0.1
- }
- if(size > 1024&&unitIndex==2){
- size = size*0.1
- }
- if(size > 480&&unitIndex==2){
- size = size*0.5
- }
- // 保留两位小数, 四舍五入
- size = Math.round(size * 10) / 10;
- return size
- }
- static copyText(text:string):boolean{
- const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN,text)
- const systemPasteboard = pasteboard.getSystemPasteboard()
- systemPasteboard.setData(pasteboardData)
- systemPasteboard.getData().then((data)=>{
- return data
- })
- return false
- }
- static getDbm(rssi:number):number{
- return Math.round((rssi / 2) + 100)
- }
- //根据数字形式的IP地址获取字符串形式的IP地址
- static getIpAddrFromNum(ipNum: number): string {
- return (ipNum >>> 24) + '.' + (ipNum >> 16 & 0xFF) + '.' + (ipNum >> 8 & 0xFF) + '.' + (ipNum & 0xFF);
- }
- static resolveIP(ip:number) {
- let address: string = ip.toString()
- if (address === '0') {
- return '00:00:000:000'
- }
- address.substring(0, 2)
- return `${address.substring(0, 2)}:${address.substring(2, 4)}:${address.substring(4, 7)}:${address.substring(7, 10)}`
- }
- static isFreeTime():boolean{
- if(DateUtil.isWeekend()){
- return true
- }
- let str = DateUtil.getFormatDateStr(new Date(), 'HH')
- let a = Number(str);
- if(isNaN(a))
- return false
- // ToastUtil.showToast('a='+a)
- if (a >= 7 && a <13) {//白天
- return false;
- }
- if (a >= 13 && a <20) {//白天
- return false;
- }
- if (a >= 0 && a < 7) {//凌晨
- return true;
- }
- if (a >= 20 && a <= 24) {//晚上
- return true;
- }
- return false
- }
- //判断是否是音乐文件
- static isMusicByExtension(filename:string) {
- const extensions = CommonConstants.REAL_MUSIC_FORMAT
- const lastIndex = filename.lastIndexOf('.');
- if (lastIndex!== -1) {
- const fileExtension = filename.slice(lastIndex).toLowerCase();
- return extensions.includes(fileExtension);
- }
- return false;
- }
- //判断是否是媒体文件 音乐和视频都可以
- static isMeidaByExtension(filename:string) {
- const extensions = CommonConstants.MEDIA_FORMAT
- const lastIndex = filename.lastIndexOf('.');
- if (lastIndex!== -1) {
- const fileExtension = filename.slice(lastIndex).toLowerCase();
- return extensions.includes(fileExtension);
- }
- return false;
- }
- //判断是否是视频文件
- static isVideoByExtension(filename:string) {
- const extensions = CommonConstants.VIDEO_FORMAT
- const lastIndex = filename.lastIndexOf('.');
- if (lastIndex!== -1) {
- const fileExtension = filename.slice(lastIndex).toLowerCase();
- return extensions.includes(fileExtension);
- }
- return false;
- }
- // 获取缩略图
- static async getFetchFrameByTime(filePath: string,time?:number,imageWidth?:number,imageHeight?:number) {
- if(Utility.isMusicByExtension(filePath)){
- return undefined
- }
- let pixelMap:image.PixelMap|undefined =undefined;
- try{
- // 创建AVImageGenerator对象
- let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator()
- let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
- let avFileDescriptor: media.AVFileDescriptor = { fd: file.fd };
- avImageGenerator.fdSrc = avFileDescriptor;
- // 初始化入参
- let timeUs = 0
- console.info('onecold time='+time)
- if(time){
- timeUs = (time > 0) ? time*50 : 0
- }
- let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC
- if(imageWidth){
- imageWidth =imageWidth/2.5
- }
- if(imageHeight){
- imageHeight = imageHeight/2.5
- }
- let param: media.PixelMapParams = {
- width : imageWidth||300,
- height : imageHeight||400,
- }
- // 获取缩略图(promise模式)
- pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param)
- // 释放资源(promise模式)
- avImageGenerator.release()
- console.info(`release success.`)
- fs.closeSync(file);
- }catch (error) {
- console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
- }
- return pixelMap
- }
- // 获取fd文件路径
- static async getFdDir(path:string){
- let fdPath = 'fd://';
- let file = await fileIo.open(path)
- fdPath = fdPath + '' + file.fd;
- return fdPath
- }
- // 根据uri获取文件名称
- static getMediaNameByUri(myUri: string) {
- let myFileName = (myUri.split('/').pop()) as string;
- return decodeURIComponent(myFileName)
- }
- static msToStandardTime(ms:string):string{
- let date = new Date(ms);
- let hours = date.getHours();
- let minutes = date.getMinutes();
- let seconds = date.getSeconds();
- return hours + ':' + minutes + ':' + seconds;
- }
- static getTimestampFromDateStr(dateStr: string, format: string = 'yyyy-MM-dd HH:mm'): number {
- // 定义正则表达式以匹配日期字符串中的各部分
- const regex = format
- .replace('yyyy', '(\\d{4})')
- .replace('MM', '(\\d{2})')
- .replace('dd', '(\\d{2})')
- .replace('HH', '(\\d{2})')
- .replace('mm', '(\\d{2})');
- const match = new RegExp(regex).exec(dateStr);
- if (!match) {
- throw new Error('Invalid date string or format');
- }
- // 解析匹配结果
- const year = Number(match[1]);
- const month = Number(match[2]);
- const day = Number(match[3]);
- const hours = Number(match[4]);
- const minutes = Number(match[5]);
- // 创建 Date 对象
- const dateObj = new Date(year, month - 1, day, hours, minutes);
- // 返回时间戳(以秒为单位)
- return Math.floor(dateObj.getTime() / 1000);
- }
- static getFormatDateStr(date: number | string | Date, format: string = 'yyyy-MM-dd HH:mm:ss'): string {
- // 将输入转换为 Date 对象
- let dateObj: Date;
- if (typeof date === 'number') {
- // 如果是十位时间戳,转换为毫秒数
- dateObj = new Date(date.toString().length === 10 ? date * 1000 : date);
- } else if (typeof date === 'string') {
- // 如果是字符串,尝试解析为 Date 对象
- dateObj = new Date(date);
- } else {
- // 如果是 Date 对象,直接使用
- dateObj = date;
- }
- // 定义替换规则
- const replacements = new Map<string, string>([
- ['yyyy', String(dateObj.getFullYear())],
- ['MM', String(dateObj.getMonth() + 1).padStart(2, '0')],
- ['dd', String(dateObj.getDate()).padStart(2, '0')],
- ['HH', String(dateObj.getHours()).padStart(2, '0')],
- ['mm', String(dateObj.getMinutes()).padStart(2, '0')],
- ['ss', String(dateObj.getSeconds()).padStart(2, '0')]
- ]);
- // 替换格式字符串中的占位符
- replacements.forEach((value, key) => {
- format = format.replace(new RegExp(key, 'g'), value);
- });
- return format;
- }
- static async getFilePixelMapBig(uri: string): Promise<string | undefined> {
- let pixelMap: image.PixelMap | undefined = undefined;
- if (Utility.isMusicByExtension(uri)) {
- pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri);
- } else {
- pixelMap = await Utility.getFetchFrameByTime(uri);
- }
- return pixelMap ? ImageUtil.pixelMapToBase64Str(pixelMap) : undefined;
- }
- static async getFilePixelMap(uri: string): Promise<string | undefined> {
- let pixelMap: image.PixelMap | undefined = undefined;
- if (Utility.isMusicByExtension(uri)) {
- pixelMap = await Utility.getFetchMetadataFromFdSrcByPromise(uri);
- } else {
- pixelMap = await Utility.getFetchFrameByTime(uri);
- }
- return pixelMap ? ImageUtil.pixelMapToBase64Str(pixelMap) : undefined;
- }
- //根据filePathe获取对应的播放Index
- static getIndexFromList(localList:Array<VideoItem>,filePath:string){
- let list: Array<string> = [];
- for(let i=0;i<localList.length;i++){
- if(localList[i].filePath === filePath){
- return i
- }
- }
- return 0
- }
- // 在以下demo中,使用资源管理接口获取打包在HAP内的媒体资源文件,通过设置fdSrc属性,获取音频元数据并打印,
- // 获取音频专辑封面并通过Image控件显示在屏幕上。该demo以Promise形式进行异步接口调用
- static async getFetchMetadataFromFdSrcByPromise(uri: string): Promise<image.PixelMap | undefined> {
- let cover: image.PixelMap | undefined = undefined;
- if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
- try {
- // 创建AVMetadataExtractor对象
- const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
- // 设置fdSrc
- const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
- avMetadataExtractor.fdSrc = fd;
- // 获取元数据(promise模式)
- const metadata = await avMetadataExtractor.fetchMetadata();
- metadata.author
- console.info(`get meta data, hasAudio: ${metadata.hasAudio}`);
- // 获取专辑封面(promise模式)
- cover = await avMetadataExtractor.fetchAlbumCover();
- // 释放资源(promise模式)
- await avMetadataExtractor.release();
- console.info('release success.');
- } catch (error) {
- console.error('Error during metadata extraction:', error);
- }
- } else {
- console.warn('AVMetadataExtractor capability is not supported.');
- }
- return cover;
- }
- static formatTimestamp(timestamp: number): string {
- // 将时间戳转换为 Date 对象
- const date = new Date(timestamp);
- // 获取各个部分
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- // 拼接成所需的格式
- return `${year}-${month}-${day} ${hours}:${minutes}`;
- }
- //获取音乐资源的属性值,
- static async uriGetMusicAssetsFromFile(context:Context,uri:string,type:number
- ,autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
- await ReqPermissionUtil.persistPermission(uri);
- //这里加个判断,如果文件的大小是0K,这个文件是空的,直接返回
- try {
- const file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
- const fileStat = await fs.stat(file.fd);
- if (fileStat.size < 1024) {// 检查是否为有效文件(>1KB)
- console.warn(`File ${uri} is empty (0KB). Returning default VideoItem.`);
- // Return a default or empty VideoItem object
- return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
- }
- } catch (error) {
- console.error(`Failed to get stats for file ${uri}:`, error);
- // If we can't read the file, still return a default item
- return new VideoItem(FileUtil.getFileName(uri), uri, uri, type, 0, '');
- }
- if(StrUtil.isNotEmpty(uri)&&uri.toLowerCase().endsWith('.cue')){
- const cueItem = new VideoItem(FileUtil.getFileName(uri),uri,uri,type,0,'')
- cueItem.fileName = FileUtil.getFileName(uri)
- return cueItem
- }
- return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName,originFileName)
- //华为官方不支持的格式 不支持内嵌歌词Dsf,aif,aiff,wav 不支持内嵌封面dsf,aif,aiff
- if(StrUtil.isNotEmpty(uri)){
- if(uri.toLowerCase().endsWith('.dsf')
- ||uri.toLowerCase().endsWith('.aif')
- // ||uri.toLowerCase().endsWith('.wav')
- ||uri.toLowerCase().endsWith('.aiff')){
- return Utility.readMetaInfoFFmpeg(context,uri,type,autoParseMusicName)
- }
- }
- let item:VideoItem = new VideoItem('',uri,uri,type,0,'')
- try {
- console.info('asset file.uri: ', uri);
- let file = fs.openSync(uri, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
- console.info("file.fd " + file.fd);
- let fdfd = 'fd://' + file.fd
- //3、通过fs.stat方法获取stat对象
- console.info('asset file.name: ', file.name);
- console.info('asset file.uri: ', uri);
- console.info('asset file.fd: ', file.fd);
- console.info('asset file.path: ', file.path);
- item = new VideoItem(file.name,uri,uri,type,0,'')
- await fs.stat(file.fd).then(async (stat: fs.Stat) => {
- console.info("get file info succeed, the size of file is " + stat.size);
- let videoSize = stat.size
- // let videoTime = stat.ctime
- let fileSize = Utility.formatFSize(videoSize)
- //按照添加时间
- let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
- // console.info('onecold asset addTime: ', addTime);
- // let cTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
- // console.info('asset stat.ctime: ', stat.ctime);
- let musicName:string | undefined = file.name
- let artist:string | undefined = ''
- let album:string | undefined = ''
- let pixelMap:image.PixelMap|undefined|null = undefined
- let imagePath = ''
- let duration:string | undefined = ''
- let mimeType:string | undefined = ''
- let trackCount:string | undefined = ''//轨道数量
- let sampleRate:string | undefined = ''//音频的采样率单位为Hz
- if (canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
- try {
- // 创建AVMetadataExtractor对象
- const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
- // 设置fdSrc
- const fd = await fs.openSync(uri, fs.OpenMode.READ_ONLY);
- avMetadataExtractor.fdSrc = fd;
- // 获取元数据(promise模式)
- const metadata = await avMetadataExtractor.fetchMetadata();
- if(StrUtil.isNotEmpty(metadata.title)){
- musicName = metadata.title
- }else{
- //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
- console.log(`onecold musicName为空:${file.name}`);
- const musicData = parseMusicFileName(file.name);
- if (musicData.isValid) {
- // console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
- // console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
- musicName = musicData.title
- if(artist==''||artist==undefined)
- artist = musicData.artist
- } else {
- musicName = file.name
- // console.log("onecold 文件名格式不符合要求");
- }
- }
- if(StrUtil.isNotEmpty(metadata.artist)){
- artist = metadata.artist
- }
- if(artist==undefined)
- artist = ''
- if(StrUtil.isNotEmpty(metadata.album)){
- album = metadata.album
- }else{
- album = ''
- }
- if(StrUtil.isNotEmpty(metadata.duration)){
- if(metadata.duration)
- duration = convertSecondsToTime(metadata.duration.toString())
- }
- if(StrUtil.isNotEmpty(metadata.mimeType)){
- mimeType = metadata.mimeType
- }
- if(StrUtil.isNotEmpty(metadata.trackCount)){
- trackCount = metadata.trackCount
- }
- if(StrUtil.isNotEmpty(metadata.sampleRate)){
- sampleRate = metadata.sampleRate
- }
- let name = await MD5.digestSync(uri)
- // if(isLoadPixelMap){
- // 获取专辑封面(promise模式)
- // pixelMap = await avMetadataExtractor.fetchAlbumCover();
- // // 释放资源(promise模式)
- // await avMetadataExtractor.release();
- pixelMap = await fetchAlbumCover(avMetadataExtractor)
- // console.info('onecold release success. name= '+musicName);
- if(pixelMap!==undefined&&pixelMap!==null){
- // console.info('onecold pixelMap is not empty= '+musicName);
- imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,name)
- imagePath = fileUri.getUriFromPath(imagePath)
- }else{
- // console.info('onecold pixelMap is empty= '+musicName);
- imagePath = ''
- // if(StrUtil.isNotEmpty(artist))
- // imagePath = await NetAxiosUtil.getLyricCover(musicName,artist)
- }
- // }else{
- // imagePath = context.filesDir + FileUtil.separator + name
- // imagePath = fileUri.getUriFromPath(imagePath)
- // }
- console.info('onecold release success. imagePath= '+imagePath);
- } catch (error) {
- console.error('Error during metadata extraction:', error);
- }
- } else {
- console.warn('AVMetadataExtractor capability is not supported.');
- }
- if(musicName==undefined)
- musicName = file.name
- item = new VideoItem(musicName,uri ,uri,type,videoSize,addTime,fileSize,
- imagePath,artist,album,file.name)
- item.duration = duration+'';
- item.mimeType = mimeType;
- item.trackCount = trackCount;
- item.sampleRate = sampleRate;
- item.isFav = 0;
- item.playCount = 0;
- item.pyStr = pinyin4js.getShortPinyin(musicName)
- // item.lyricContent = await extractLyricsContent(uri)
- let metaItem = await parseAudioMetadata(uri)
- if(metaItem){
- item.lyricContent = metaItem.lyricContent
- item.bit_rate = formatBitrateToKbps(metaItem.bit_rate || "0");
- item.year = metaItem.year ||'unknown'
- item.probe_score = metaItem.probe_score
- item.nb_streams = metaItem.nb_streams
- item.nb_programs = metaItem.nb_programs
- }
- })
- } catch (error) {
- console.error('uriGetAssetsFromFile failed with err: ' + JSON.stringify(error));
- }
- return item
- }
- static async readMetaInfoFFmpeg(context:Context,inputPath: string,type:number,
- autoParseMusicName?:boolean,originFileName?:string): Promise<VideoItem> {
- return new Promise((resolve, reject) => {
- inputPath = FileUtil.getFilePath(inputPath)
- let commands = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", inputPath];
- let outputJson = "";
- FFmpeg.execute(commands, {
- logCallback: (logLevel: number, logMessage: string) => console.log(`[${logLevel}]${logMessage}`),
- outputCallback: (message: string) => {
- outputJson += message;
- },
- }).then(async () => {
- try {
- const metadata: FFprobeMetadata = JSON.parse(outputJson);
- const format = metadata.format;
- console.log(`onecold outputJson:${outputJson}`);
- let file = fs.openSync(inputPath, fs.OpenMode.READ_ONLY | fs.OpenMode.CREATE)
- console.info('readMetaInfoFFmpeg asset file.path: ', file.path);
- let videoItem = new VideoItem(file.name,inputPath,inputPath,type,0,'')
- await fs.stat(file.fd).then(async (stat: fs.Stat) => {
- // 获取音频流的采样率
- let sampleRate:string|undefined = '';
- let bits_per_raw_sample:string|undefined = '';
- let channels:string|undefined = '';
- let channel_layout:string|undefined = '';
- let start_time:string|undefined = '';
- if (metadata.streams && metadata.streams.length > 0) {
- // 查找第一个音频流
- const audioStream = metadata.streams.find(stream => StrUtil.isNotEmpty(stream.sample_rate));
- if (audioStream) {
- sampleRate = audioStream.sample_rate;
- bits_per_raw_sample = audioStream.bits_per_raw_sample
- channel_layout = audioStream.channel_layout
- channels = audioStream.channels
- start_time = audioStream.start_time
- }
- }
- let videoSize = stat.size
- const modifyTime = Utility.getFormatDateStr(stat.mtime,'yyyy-MM-dd HH:mm')
- let fileSize = Utility.formatFSize(videoSize)
- //按照添加时间
- let addTime = Utility.getFormatDateStr(new Date(),'yyyy-MM-dd HH:mm');
- // Extract artist and title from tags
- const tags = format.tags || {};
- let artist = tags.artist ||tags.ARTIST || '';
- let title = tags.title ||tags.TITLE || '';
- let album = tags.album ||tags.ALBUM|| '';
- Logger.info('readMetaInfoFFmpeg', `原始标签 title="${title}", artist="${artist}", album="${album}"`);
- console.log(`onecold tags.tags:${JSON.stringify(tags)}`);
- // 检查是否有乱码
- if (hasGarbledText(tags)) {
- // 第二次尝试用华为的接口提取元数据
- let hwTags = await extractHwMediaMetadata(inputPath)
- artist = hwTags?.artist|| '';
- title = hwTags?.title|| '';
- album = hwTags?.album|| '';
- console.info('onecold 乱码修正 artist='+artist);
- console.info('onecold 乱码修正 title='+title);
- console.info('onecold 乱码修正 album='+album);
- }
- Logger.info('readMetaInfoFFmpeg', `最终标签 title="${title}", artist="${artist}", album="${album}", autoParse=${autoParseMusicName}`);
- if(StrUtil.isEmpty(title)){
- //自动解析像这样的获取不到元数据的 周杰伦-七里香.mp3文件
- console.log(`onecold musicName为空:${file.name}`);
- // let autoParseMusicName = PreferencesUtil.getBooleanSync('autoParseMusicName',false)
- if(autoParseMusicName){
- const musicData = parseMusicFileName(file.name);
- if (musicData.isValid) {
- console.log(`onecold 艺术家:${musicData.artist}`); // 输出:周杰伦
- console.log(`onecold 歌曲名:${musicData.title}`); // 输出:七里香
- title = musicData.title
- if(artist==''||artist==undefined)
- artist = musicData.artist
- } else {
- title = originFileName||file.name
- }
- }else{
- title = originFileName||file.name
- }
- }
- let name: string = title;
- if (!name) {
- name = getFileNameWithoutExtension(inputPath);
- }
- let pixelMap:image.PixelMap|undefined|null = undefined
- // Create VideoItem
- videoItem = new VideoItem(
- name,
- inputPath, // id can be generated or left empty
- inputPath,
- type, // assuming it's local
- videoSize,
- addTime // convert to ISO string
- );
- // Set additional properties from format metadata
- videoItem.artist = artist;
- videoItem.album = album;
- videoItem.sampleRate = sampleRate
- videoItem.pyStr = pinyin4js.getShortPinyin(name)
- videoItem.fileName = FileUtil.getFileName(inputPath);
- if(format.duration)
- videoItem.duration = formatDuration(format.duration.toString()||'00:00')
- videoItem.size = fileSize;
- videoItem.bit_rate =formatBitrateToKbps(format.bit_rate || "0");
- videoItem.probe_score = format.probe_score;
- videoItem.nb_streams = format.nb_streams;
- videoItem.nb_programs = format.nb_programs;
- videoItem.year = tags.TYER || tags.date ||tags.DATE|| ''; // try different tag names for year
- videoItem.lyricContent = tags.LYRICS || tags.lyrics || tags.USLT || tags.UNSYNCEDLYRICS || '';
- //console.info('readMetaInfoFFmpeg videoItem.lyricContent: ', videoItem.lyricContent);
- // 如果标准字段没有歌词,则尝试解析 `lyrics-` 字段
- // tags 必须是 Record<string, string | undefined>(或 any)
- // 如果还是没有歌词内容,尝试查找自定义的lyrics-开头的属性(电脑版音乐标签内嵌歌词就是lyrics-XXX)
- if(StrUtil.isEmpty( videoItem.lyricContent)){
- const tagsRecord = tags as Record<string, string>;
- const possibleKeys = Object.keys(tagsRecord);
- for (let i = 0; i < possibleKeys.length; i++) {
- const key = possibleKeys[i];
- if (key && key.toLowerCase().startsWith('lyrics-')) {
- //console.info('readMetaInfoFFmpeg videoItem.key: ', key);
- // 通过转换后的 Record 类型安全访问属性
- videoItem.lyricContent = tagsRecord[key];
- //console.info('readMetaInfoFFmpeg videoItem.lyricContent2: ', videoItem.lyricContent);
- if (videoItem.lyricContent) {
- break;
- }
- }
- }
- }
- videoItem.genre = tags.genre||tags.GENRE|| '';
- videoItem.track = tags.track||tags.TRACK|| '';
- videoItem.ALBUMARTIST = tags.ALBUMARTIST||tags.album_artist||tags.albumartist||tags.TPE2|| '';
- videoItem.COMPOSER = tags.COMPOSER||tags.composer|| '';
- videoItem.LYRICIST = tags.LYRICIST||tags.lyricist||tags.TEXT ||'';
- videoItem.COMMENT = tags.COMMENT||tags.comment||tags.COMM|| '';
- videoItem.disc = tags.disc|| '';
- videoItem.bits_per_raw_sample = bits_per_raw_sample ||'1'
- videoItem.channel_layout = channel_layout || ''
- videoItem.channels = channels ||'0'
- videoItem.start_time = start_time || '00:00'
- videoItem.mimeType = format.format_name
- // 检查是否有封面图片流
- const hasCover = metadata.streams.some(stream =>
- stream.disposition?.attached_pic === 1
- );
- console.info('onecold readMetaInfoFFmpeg asset hasCover: ', hasCover);
- let md5Name = await MD5.digestSync(inputPath)
- let imagePath = `${context.filesDir}${FileUtil.separator}${md5Name}.jpg`;
- console.info('onecold readMetaInfoFFmpeg asset imagePath: ', imagePath);
- // 如果有封面图片,则提取
- try {
- if (hasCover) {
- //提取封面
- let isSuccess:boolean= await getFFmpegCover(inputPath, imagePath);
- if(isSuccess){
- imagePath = fileUri.getUriFromPath(imagePath)
- }else{
- // 使用华为接口提取封面(备用方案)
- console.warn('onecold 提取封面图片失败,使用华为接口提取封面');
- imagePath = await extractCoverWithHwInterface(inputPath, context, name);
- }
- videoItem.pixelMapPath = imagePath;
- }else if(Utility.isVideoByExtension(inputPath)){
- pixelMap = await Utility.getFetchFrameByTime(inputPath)
- if(pixelMap!==undefined&&pixelMap!==null){
- imagePath = await ImageUtil.savePixelMap(pixelMap,context.filesDir,md5Name)
- imagePath = fileUri.getUriFromPath(imagePath)
- }else{
- imagePath = ''
- }
- imagePath = fileUri.getUriFromPath(imagePath)
- videoItem.pixelMapPath = imagePath;
- }
- } catch (error) {
- console.warn('onecold 提取封面图片失败:', error.message);
- }
- if(sampleRate&&format.bit_rate){
- videoItem.md5Str = determineAudioQuality( videoItem.mimeType,Number(format.bit_rate),Number(sampleRate))
- }
- console.info('onecold Successfully parsed metadata:', videoItem);
- resolve(videoItem);
- })
- } catch (error) {
- console.error('onecold Failed to parse metadata:', error);
- reject(new Error('onecold Failed to parse metadata: ' + error.message));
- }
- }).catch((error: Error) => {
- console.error(`onecold Execution failed with error: ${error.message}`);
- reject(error);
- });
- });
- }
- private completionNum(num: number): string | number {
- if (num < 10) {
- return '0' + num;
- } else {
- return num;
- }
- }
- //根据字节获取大小
- static formatFSize(bytes:number):string {
- const units = ['Bytes', 'K', 'M', 'G'];
- let size = bytes;
- let unitIndex = 0;
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
- // 保留两位小数, 四舍五入
- size = Math.round(size * 10) / 10;
- return size + units[unitIndex]
- }
- static gotoMarket(context:common.UIAbilityContext,bundleName:string) {
- const isAvailable = canIUse('SystemCapability.AppGalleryService.Distribution.Comment');
- if(isAvailable) {
- try {//需要api 20 才可以调用用户评论对话框showCommentDialog
- commentManager.showCommentDialog(context).then(()=>{
- hilog.info(0, 'TAG', "succeeded in showing commentDialog.");
- return
- }).catch((error: BusinessError<Object>) => {
- Utility.goMarket(context,bundleName)
- hilog.error(0, 'TAG', `showCommentDialog failed, Code: ${error.code}, message: ${error.message}`);
- });
- } catch (error) {
- Utility.goMarket(context,bundleName)
- hilog.error(0, 'TAG', `showCommentDialog failed, Code: ${error.code}, message: ${error.message}`);
- }
- }else{
- Utility.goMarket(context,bundleName)
- }
- }
- static async goMarket(context:common.UIAbilityContext,bundleName:string){
- const want: Want = {
- uri: `store://appgallery.huawei.com/app/detail?id=${bundleName}`
- };
- context.startAbility(want).then(()=>{
- //拉起成功
- }).catch(()=>{
- // 拉起失败
- });
- }
- static async doShare(item:VideoItem,context:common.UIAbilityContext){
- // 生成视频封面图
- const imagePackerApi: image.ImagePacker = image.createImagePacker();
- const buffer: ArrayBuffer = await (imagePackerApi.packing(await Utility.getFetchFrameByTime(item.filePath), {
- format: 'image/jpeg',
- quality: 30
- }) as Promise<ArrayBuffer>);
- // 构造ShareData,需配置一条有效数据信息
- let shareData: systemShare.SharedData = new systemShare.SharedData({
- utd: uniformTypeDescriptor.UniformDataType.MEDIA,
- uri: fileUri.getUriFromPath(item.filePath),
- title: item.name, // 不传title字段时,显示视频文件名
- // description: '好听的音乐', // 不传description字段时,显示视频大小
- thumbnail: new Uint8Array(buffer), // 优先使用传递的缩略图做预览 不传则默认使用视频第一帧画面做预览图
- });
- // 进行分享面板显示
- let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
- // let context = getContext(this) as common.UIAbilityContext;
- controller.show(context, {
- selectionMode: systemShare.SelectionMode.SINGLE,
- previewMode: systemShare.SharePreviewMode.DETAIL,
- }).then(() => {
- console.info('ShareController show success.');
- }).catch((error: BusinessError) => {
- console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
- });
- }
- //分享索引index的视频
- static async doShareMusic(item:VideoItem,context:common.UIAbilityContext){
- // 构造ShareData,需配置一条有效数据信息
- let shareData: systemShare.SharedData = new systemShare.SharedData({
- utd: uniformTypeDescriptor.UniformDataType.AUDIO,
- uri: fileUri.getUriFromPath(item.filePath),
- title: item.name, // 不传title字段时,显示视频文件名
- description: '好听的音乐', // 不传description字段时,显示视频大小
- });
- // 进行分享面板显示
- let controller: systemShare.ShareController = new systemShare.ShareController(shareData);
- // let context = getContext(this) as common.UIAbilityContext;
- controller.show(context, {
- selectionMode: systemShare.SelectionMode.SINGLE,
- previewMode: systemShare.SharePreviewMode.DETAIL,
- }).then(() => {
- console.info('ShareController show success.');
- }).catch((error: BusinessError) => {
- console.error(`ShareController show error. code: ${error.code}, message: ${error.message}`);
- });
- }
- static getNameList(localList:Array<VideoItem>){
- let list: Array<string> = [];
- for(let i=0;i<localList.length;i++){
- if(StrUtil.isNotEmpty(localList[i].name))
- list.push(localList[i].name);
- }
- return list
- }
- //获取列表有封面的值
- static getFirstCoverFromList(localList:Array<VideoItem>):string{
- let list: Array<string> = [];
- for(let i=0;i<localList.length;i++){
- let pix = localList[i].pixelMapPath
- if(pix){
- return pix
- }
- }
- return ''
- }
- //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_Dir:私密视频)
- static getGlobalNameList(localList:Array<VideoItem>,type:number){
- let list: Array<string> = [];
- for(let i=0;i<localList.length;i++){
- if(localList[i].type === type){
- list.push(localList[i].name);
- }
- }
- return list
- }
- //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
- static getGlobalList(localList:Array<VideoItem>,type:number){
- let list: Array<VideoItem> = [];
- for(let i=0;i<localList.length;i++){
- //播放列表过滤.cue文件
- if(localList[i].type === type&&!localList[i].filePath.toLowerCase().endsWith('.cue') ){
- list.push(localList[i]);
- }
- }
- return list
- }
- //以下是实现 getPlayItem 方法的代码,根据 playType 的值返回随机项或第一项
- static getPlayItem(localList: Array<VideoItem>, playType: number): VideoItem | undefined {
- // 处理空列表情况
- if (!localList || localList.length === 0) {
- return undefined;
- }
- // 当 playType 为 3 时随机返回一个元素
- if (playType === 3) {
- const randomIndex = Math.floor(Math.random() * localList.length);
- return localList[randomIndex];
- }
- // 其他情况返回第一个元素
- else {
- return localList[0];
- }
- }
- /**
- * 根据文件路径从项数组中查找对应的VideoItem
- * @param list VideoItem数组
- * @param filePath 文件路径
- * @returns 对应的VideoItem,如果未找到则返回undefined
- */
- static getItemByFilePath(list: Array<VideoItem>, filePath: string): VideoItem | undefined {
- if (!list || list.length === 0 || !filePath) {
- return undefined;
- }
- for (let i = 0; i < list.length; i++) {
- if (list[i].filePath === filePath) {
- return list[i];
- }
- }
- return undefined;
- }
- //根据filePath获取当前索引index
- static getCurIndexFromGlobalList(localList:Array<VideoItem>,filePath:string){
- let index: number = 0;
- for(let i=0;i<localList.length;i++){
- if(localList[i].filePath === filePath){
- index = i
- return index
- }
- }
- return index
- }
- //新的查询是否收藏的方法
- static getIsFav(favList: Array<VideoItem>,item:VideoItem|undefined){
- if(ArrayUtil.isEmpty(favList)){
- return false
- }
- if(item===undefined){
- return false
- }
- for(let i=0;i<favList.length;i++){
- if(favList[i].filePath === item.filePath&&favList[i].isFav===1){
- return true
- }
- }
- return false
- }
- static getIsFac(facList: Array<String>,item:VideoItem|undefined){
- if(ArrayUtil.isEmpty(facList)){
- return false
- }
- if(item===undefined){
- return false
- }
- for(let i=0;i<facList.length;i++){
- if(facList[i] === item.name||facList[i]===item.fileName){
- return true
- }
- }
- return false
- }
- static resourceToString(context:Context,resource:Resource){
- if(!context||!resource)
- return ''
- return context.resourceManager.getStringSync(resource).toString()
- }
- //根据type获取对应的播放列表(CommonConstants.TYPE_LOCAL:本地,CommonConstants.TYPE_LOCK:私密视频)
- static getGlobalMusicList(localList:Array<VideoItem>,type:number){
- let list: Array<VideoItem> = [];
- for(let i=0;i<localList.length;i++){
- if(localList[i].type === type){
- list.push(localList[i]);
- }
- }
- return list
- }
- static isHaoFreeTime():boolean{
- if(DateUtil.isWeekend()){
- return true
- }
- let str = DateUtil.getFormatDateStr(new Date(), 'HH')
- let a = Number(str);
- if(isNaN(a))
- return false
- // ToastUtil.showToast('a='+a)
- if (a >= 8 && a <13) {//白天
- return false;
- }
- if (a >= 13 && a <20) {//白天
- return false;
- }
- if (a >= 0 && a < 8) {//凌晨
- return true;
- }
- if (a >= 20 && a <= 24) {//晚上
- return true;
- }
- return false
- }
- static getParentDirectory(filePath: string): string {
- // 使用正则表达式匹配路径分隔符(支持 Unix 和 Windows)
- const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
- if (lastSlashIndex === -1) {
- return '';
- }
- return filePath.substring(0, lastSlashIndex);
- }
- // 开启沉浸式显示模式
- static async enableFullScreen() {
- const ctx = getContext()
- const win = await window.getLastWindow(ctx)
- win.setWindowLayoutFullScreen(true)
- const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM)
- AppStorage.setOrCreate('topHeight', px2vp(top.topRect.height))
- AppStorage.setOrCreate('bottomHeight', px2vp(top.bottomRect.height))
- }
- // 关闭沉浸式显示模式
- static async disableFullScreen() {
- const ctx = getContext()
- const win = await window.getLastWindow(ctx)
- win.setWindowLayoutFullScreen(false)
- AppStorage.setOrCreate('topHeight', 0)
- AppStorage.setOrCreate('bottomHeight', 0)
- }
- // 设置状态栏文字颜色为白色
- static async setStatusBarLight() {
- const ctx = getContext()
- const win = await window.getLastWindow(ctx)
- win.setWindowSystemBarProperties({
- statusBarContentColor: '#ffffff'
- })
- }
- // 设置状态栏文字颜色为黑色
- static async setStatusBarDark() {
- const ctx = getContext()
- const win = await window.getLastWindow(ctx)
- win.setWindowSystemBarProperties({
- statusBarContentColor: '#000000'
- })
- }
- // 优化点:完全基于Promise链的异步处理
- static async getAppName(context: Context): Promise<string> {
- try {
- // 1. 同步化BundleInfo获取
- const bundleInfo = await bundleManager.getBundleInfoForSelf(
- bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
- );
- // 2. 安全获取labelId(新增空值校验)
- const labelId = bundleInfo.appInfo?.labelId;
- if (!labelId || labelId <= 0) {
- throw new Error("Invalid labelId: " + labelId);
- }
- // 3. 异步资源解析(替换危险的getStringSync)
- return await context.resourceManager.getStringValue(labelId);
- } catch (err) {
- console.error(`[${new Date().toISOString()}] AppName Error: CODE=${err.code}, MSG=${err.message}`);
- // 4. 多级降级策略
- return AppUtil.getBundleName() // 最终兜底
- }
- }
- //按名称升序
- static doSortListAscending( list:Array<VideoItem>,isShowFileName?:boolean){
- let options: Intl.CollatorOptions = {
- localeMatcher: "lookup",
- usage: "sort",
- sensitivity: "accent", // 对中文更友好
- numeric:true
- };
- const collator = new Intl.Collator("zh-CN", options);
- list.sort((a, b) => {
- const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
- if (typeOrder !== 0) {
- return typeOrder;
- }
- if(isShowFileName){
- return collator.compare(a.fileName||a.name, b.fileName||b.name)
- }else{
- return collator.compare(a.name, b.name)
- }
- });
- }
- //按名称降序
- static doSortListDescending(list:Array<VideoItem>,isShowFileName?:boolean){
- const options: Intl.CollatorOptions = {
- localeMatcher: "lookup",
- usage: "sort",
- sensitivity: "accent", // 对中文更友好
- numeric:true
- };
- const collator = new Intl.Collator("zh-CN", options);
- list.sort((a, b) => {
- const typeOrder = getTypeOrder(a.type) - getTypeOrder(b.type);
- if (typeOrder !== 0) {
- return typeOrder;
- }
- if(isShowFileName){
- return collator.compare(b.fileName||b.name, a.fileName||a.name)
- }else{
- return collator.compare(b.name, a.name)
- }
- // const partsA = extractParts(a.name);
- // const partsB = extractParts(b.name);
- //
- // const nonNumericComparison = collator.compare(partsB.nonNumeric, partsA.nonNumeric);
- // if (nonNumericComparison !== 0) {
- // return nonNumericComparison;
- // }
- // return partsB.numeric - partsA.numeric;
- });
- }
- /**
- * 读取本地会员到期时间(解密.vv文件)
- * @returns
- */
- static async getLocalNobleExpireDate(): Promise<string> {
- // return "2025-08-08 12:00";
- try {
- const documentViewPicker = new picker.DocumentViewPicker()
- let documentSaveResult = await documentViewPicker.save({ pickerMode: picker.DocumentPickerMode.DOWNLOAD })
- let download_path = new fileUri.FileUri(documentSaveResult[0]).path + '/'
- let filePath = download_path + STR_LOCK_VIDEO+ '/' + VIP_FILEPATH
- let expireDate = Utility.readDataFromFile(filePath)
- return expireDate;
- } catch (e) {
- return '';
- }
- }
- static readDataFromFile(filePath: string) {
- if(!FileUtil.accessSync(filePath)){
- return ''
- }
- let readTextOptions: ReadTextOptions = {
- offset: 0,
- length: 0,
- encoding: 'utf-8'
- };
- let stat = fileIo.statSync(filePath);
- readTextOptions.length = stat.size;
- let str = fileIo.readTextSync(filePath, readTextOptions);
- const parsedData:VipData = JSON.parse(str) as VipData;
- // LogUtil.info('onecold parsedData.expireDate = ' + parsedData.expireDate)
- let result = Base64Util.decodeSync( parsedData.expireDate)
- // LogUtil.info('onecold 解密的result = ' +StrUtil.unit8ArrayToStr(result))
- return StrUtil.unit8ArrayToStr(result);
- }
- }
- interface PathStat {
- path: string;
- isDirectory: boolean;
- }
- async function isDirectory(filePath: string): Promise<boolean> {
- try {
- const stat = await fs.stat(filePath);
- return stat.isDirectory();
- } catch (error) {
- console.error('Error getting file stat:', error);
- return false;
- }
- }
- //排序模式参数
- interface VideoNameParts {
- nonNumeric: string;
- numeric: number;
- suffix:string;
- }
- function extractParts(name: string): VideoNameParts {
- // 更完善的正则表达式,处理各种情况
- // 匹配:非数字前缀 + 数字部分 + 可选的剩余部分
- const match = name.match(/^(\D*)(\d+)(.*)$/);
- if (match) {
- return {
- nonNumeric: match[1] || '', // 非数字前缀
- numeric: parseInt(match[2], 10), // 数字部分
- suffix: match[3] || '' // 剩余部分(可能包含扩展名等)
- };
- }
- // 如果没有找到数字模式,将整个名称作为非数字部分
- return {
- nonNumeric: name,
- numeric: 0,
- suffix: ''
- };
- }
- function getInstallTime(): number | null {
- // 从本地存储中获取安装时间
- return PreferencesUtil.getNumberSync('installTime')
- }
- function setInstallTime(time: number) {
- // 将安装时间存储到本地存储中
- PreferencesUtil.putSync('installTime',time)
- }
- function fetchAlbumCover(avMetadataExtractor: media.AVMetadataExtractor): Promise<image.PixelMap | null> {
- return new Promise((resolve, reject) => {
- avMetadataExtractor.fetchAlbumCover((error: BusinessError, pixelMap: image.PixelMap) => {
- if (error) {
- console.error(`Failed to fetch AlbumCover, error = ${JSON.stringify(error)}`);
- resolve(null);
- } else {
- resolve(pixelMap);
- }
- avMetadataExtractor.release();
- });
- });
- }
- //排序类型
- function getTypeOrder(type: number) {
- switch (type) {
- case CommonConstants.TYPE_IS_DIR:
- return 1; // First
- case CommonConstants.TYPE_IS_CSJAD:
- return 2; // Middle
- case CommonConstants.TYPE_LOCAL:
- return 3; // Last
- default:
- return 4; // Unknown types, if any, go last
- }
- }
- function convertSecondsToTime(secondsStr: string): string {
- if (!secondsStr || isNaN(Number(secondsStr))) {
- return "00:00";
- }
- let seconds = parseInt(secondsStr, 10);
- seconds = Math.floor(seconds / 1000);
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = seconds % 60;
- const formatNumber = (num: number) => num.toString().padStart(2, '0');
- if (hours > 0) {
- return `${formatNumber(hours)}:${formatNumber(minutes)}:${formatNumber(secs)}`;
- } else {
- return `${formatNumber(minutes)}:${formatNumber(secs)}`;
- }
- }
- /**
- * 秒数 → 智能时间格式(自动选择 MM:SS 或 HH:MM:SS)
- * @param seconds 秒数字符串(如 "283.524351" 或 "3675")
- * @param forceHHMMSS 强制使用 HH:MM:SS 格式(默认自动判断)
- * @returns 格式化后的时间字符串
- */
- function formatDuration(seconds: string, forceHHMMSS: boolean = false): string {
- // 1. 校验输入
- const secNum = parseFloat(seconds);
- if (isNaN(secNum) || secNum < 0) return forceHHMMSS ? "00:00:00" : "00:00";
- // 2. 计算时间分量
- const totalSec = Math.floor(secNum);
- const hours = Math.floor(totalSec / 3600);
- const mins = Math.floor((totalSec % 3600) / 60);
- const secs = totalSec % 60;
- // 3. 格式化输出
- const pad = (n: number) => n.toString().padStart(2, '0');
- return forceHHMMSS || hours > 0
- ? `${pad(hours)}:${pad(mins)}:${pad(secs)}` // HH:MM:SS
- : `${pad(mins)}:${pad(secs)}`; // MM:SS
- }
- /**
- * 定义解析结果的数据结构
- */
- export class MusicInfo {
- artist: string = ""; // 艺术家名称
- title: string = ""; // 歌曲名称
- isValid: boolean = false; // 格式是否有效
- }
- /**
- * 精简版常见中文姓氏(音乐场景优化版)
- */
- const COMMON_CHINESE_SURNAMES = [
- // 超常见姓氏(覆盖约80%人口)
- '李','王','张','刘','陈','杨','黄','赵','吴','周',
- '徐','孙','马','朱','胡','林','郭','何','高','罗',
- '郑','梁','谢','宋','唐','许','韩','邓','曹','彭',
- '曾','萧','田','董','潘','袁','于','蔡','余','杜',
- // 音乐行业常见姓氏(歌手高频姓)
- '汪','苏','薛','谭','华','金','魏','陶','姜','窦',
- '章','毛','易','方','宋','任','沈','贾','江','孔',
- // 组合/乐队常见字
- '羽','泉','信','乐','花','飞','龙','风','云','草'
- ];
- /**
- * 歌手特征关键词(优化版)
- */
- const ARTIST_KEYWORDS = [
- // 中文特征
- '乐队','组合','乐团','和','&','合唱',' featuring',
- // 英文特征
- 'feat','ft','with','vs','presents','presents',
- // 符号特征
- '×','X','※','◆','♫'
- ];
- /**
- * 智能解析音乐文件名(支持多种分隔符和格式)
- * @param fileName - 待解析的完整文件名
- * @returns 结构化音乐信息
- */
- /**
- * 智能解析音乐文件名
- */
- /**
- * 智能解析音乐文件名(改进版)
- */
- export function parseMusicFileName(fileName: string): MusicInfo {
- const result: MusicInfo = new MusicInfo();
- if (!fileName) return result;
- // 预处理
- const cleanName: string = fileName.trim();
- const lastDotIndex: number = cleanName.lastIndexOf('.');
- const baseName: string = lastDotIndex > 0 ?
- cleanName.substring(0, lastDotIndex).trim() :
- cleanName;
- // 支持的分隔符(明确定义类型)
- const separators: string[] = ['-', '-', '—', '~', '~'];
- // 寻找分隔位置
- let bestSplitIndex: number = -1;
- for (const sep of separators) {
- const index: number = baseName.lastIndexOf(sep);
- if (index > bestSplitIndex) {
- bestSplitIndex = index;
- }
- }
- // 分割字符串
- if (bestSplitIndex > 0 && bestSplitIndex < baseName.length - 1) {
- let part1: string = baseName.substring(0, bestSplitIndex).trim();
- let part2: string = baseName.substring(bestSplitIndex + 1).trim();
- // 处理前缀序号
- part1 = part1.replace(/^\d+[\s\.\-- —~~]*/, '').trim();
- // 判断歌手部分(改进版)
- const identifyArtist = (str: string): boolean => {
- // 检查是否包含常见歌手关键词
- if (ARTIST_KEYWORDS.some((keyword: string) => str.includes(keyword))) {
- return true;
- }
- // 检查是否以常见姓氏开头
- return COMMON_CHINESE_SURNAMES.some((surname: string) =>
- str.startsWith(surname) ||
- new RegExp(`[ ,,、&&]${surname}`).test(str)
- );
- };
- // 判断歌手位置(改进逻辑)
- const part1IsArtist: boolean = identifyArtist(part1);
- const part2IsArtist: boolean = identifyArtist(part2);
- // 特殊处理:如果两个部分都识别为艺术家或都不是艺术家
- if ((part1IsArtist && part2IsArtist) || (!part1IsArtist && !part2IsArtist)) {
- // 基于常见模式判断:通常"歌曲名 - 歌手"格式更常见
- // 或者根据长度判断,歌手名通常较短
- if (part2.length <= part1.length && identifyArtist(part2)) {
- result.artist = part2;
- result.title = part1;
- } else {
- result.artist = part1;
- result.title = part2;
- }
- } else if (part1IsArtist && !part2IsArtist) {
- result.artist = part1;
- result.title = part2;
- } else if (part2IsArtist && !part1IsArtist) {
- result.artist = part2;
- result.title = part1;
- } else {
- // 默认处理
- result.artist = part1;
- result.title = part2;
- }
- // 后处理
- result.title = result.title
- .replace(/(?:\(|()[^))]*(?:)|\))/g, '')
- .replace(/\s*[—-]\s*(?:Live|Version|Remix|伴奏).*/i, '')
- .trim();
- // 有效性验证
- result.isValid = result.artist.length > 0 &&
- result.title.length > 0;
- } else {
- result.title = baseName;
- result.isValid = false;
- }
- return result;
- }
- function getFileNameWithoutExtension(filePath: string): string {
- const fileName = filePath.split('/').pop() || '';
- const lastDotIndex = fileName.lastIndexOf('.');
- return lastDotIndex > 0 ? fileName.substring(0, lastDotIndex) : fileName;
- }
- /**
- * 从音乐文件中提取封面
- * @param inputPath 音乐文件路径
- * @returns Promise<void>
- */
- // async function getFFmpegCover(inputPath: string, outputPath: string) : Promise<boolean>{
- // let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
- // FFmpeg.execute(commands, {
- // logCallback: (logLevel: number, logMessage: string) => {
- // console.info(`[FFmpeg LOG] [${logLevel}]${logMessage}`)
- // },
- // progressCallback: (message: string) => {
- // console.info(`[FFmpeg progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
- // },
- // })
- // .then(() => {
- // return true
- // console.info("FFmpeg execution succeeded.");
- // })
- // .catch((error: Error) => {
- // return false
- // console.error(`FFmpeg execution failed with error: ${error.message}`);
- // });
- // return ;
- // }
- /**
- * 使用华为接口提取音乐文件封面(备用方案)
- * @param inputPath 音乐文件路径
- * @param context 应用上下文
- * @param name 文件名称(用于保存)
- * @returns Promise<string> 返回封面图片的URI路径,失败返回空字符串
- */
- async function extractCoverWithHwInterface(inputPath: string, context: Context, name: string): Promise<string> {
- try {
- // 创建AVMetadataExtractor对象
- const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
- // 设置fdSrc
- const fd = await fs.openSync(inputPath, fs.OpenMode.READ_ONLY);
- avMetadataExtractor.fdSrc = fd;
- // 获取专辑封面
- const pixelMap = await fetchAlbumCover(avMetadataExtractor);
- // 释放资源(promise模式)
- await avMetadataExtractor.release();
- console.info('onecold release success. name= ' + name);
- if (pixelMap !== undefined && pixelMap !== null) {
- const imagePath = await ImageUtil.savePixelMap(pixelMap, context.filesDir, name);
- return fileUri.getUriFromPath(imagePath);
- } else {
- return '';
- }
- } catch (error) {
- console.error('华为接口提取封面失败:', error instanceof Error ? error.message : String(error));
- return '';
- }
- }
- /**
- * 从音乐文件中提取封面
- * @param inputPath 音乐文件路径
- * @param outputPath 封面输出路径
- * @returns Promise<boolean> 表示操作是否成功
- */
- async function getFFmpegCover(inputPath: string, outputPath: string): Promise<boolean> {
- let commands = ["ffmpeg", "-y","-i", inputPath, "-map", "0:v", "-c:v", "copy", outputPath];
- try {
- await FFmpeg.execute(commands, {
- logCallback: (logLevel: number, logMessage: string) => {
- console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
- },
- progressCallback: (message: string) => {
- console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
- },
- });
- console.info("FFmpeg execution succeeded.");
- return true;
- } catch (error) {
- console.error(`FFmpeg execution failed with error: ${error instanceof Error ? error.message : String(error)}`);
- try {
- let primaryCommands = [
- "ffmpeg", "-y",
- "-i", inputPath,
- "-map", "0:2", // 直接提取第三个流(Stream #0:2),这是附加图片流
- "-c:v", "copy", // 直接复制流数据
- outputPath
- ];
- await FFmpeg.execute(primaryCommands, {
- logCallback: (logLevel: number, logMessage: string) => {
- console.info(`[FFmpeg LOG] [${logLevel}] ${logMessage}`);
- },
- progressCallback: (message: string) => {
- console.info(`[FFmpeg progress] ${JSON.stringify(FFProgressMessageParser.parse(message))}`);
- },
- });
- console.info("FFmpeg 2 execution succeeded.");
- return true;
- }catch (error) {
- console.error(`FFmpeg execution 2 failed with error: ${error instanceof Error ? error.message : String(error)}`);
- return false;
- }
- }
- }
- /**
- * 从视频文件中提取封面 提前视频第5帧的封面
- * @param inputPath 文件路径
- * @returns Promise<void>
- */
- async function getVideoFFmpegCover(inputPath: string, outputPath: string) {
- // 更可靠的命令,专门提取视频第一帧作为封面
- const commands = [
- "ffmpeg",
- "-y", // 覆盖输出文件(如果存在)
- "-i", inputPath, // 输入文件
- "-ss", "00:00:00", // 定位到开始位置
- "-vframes", "1", // 只提取1帧
- "-q:v", "2", // 高质量JPEG(1-31,2是最高质量)
- "-f", "image2", // 强制输出为图像格式
- outputPath
- ];
- FFmpeg.execute(commands, {
- logCallback: (logLevel: number, logMessage: string) => {
- console.info(`[FFmpegX LOG] [${logLevel}]${logMessage}`)
- },
- progressCallback: (message: string) => {
- console.info(`[FFmpegX progress]${JSON.stringify(FFProgressMessageParser.parse(message))}`)
- },
- })
- .then(() => {
- })
- .catch((error: Error) => {
- console.error(`FFmpegX execution failed with error: ${error.message}`);
- });
- }
- /**
- * 从音乐文件中提取歌词内容
- * @param inputPath 音乐文件路径
- * @returns Promise<string> 直接返回歌词内容,若无歌词则返回空字符串
- */
- async function extractLyricsContent(inputPath: string): Promise<string> {
- return new Promise(async (resolve, reject) => {
- try {
- // 1. 使用ffprobe获取元数据
- const commands = [
- 'ffprobe',
- '-v', 'quiet',
- '-print_format', 'json',
- '-show_format',
- inputPath
- ];
- let outputJson = '';
- await FFmpeg.execute(commands, {
- outputCallback: (message: string) => outputJson += message,
- });
- // 2. 解析歌词标签
- const metadata:FFprobeMetadata = JSON.parse(outputJson);
- const tags = metadata.format?.tags || {};
- // 3. 从常见标签中查找歌词(优先级顺序)
- const lyricContent =
- tags.LYRICS || // 标准标签
- tags.lyrics || // 小写变体
- tags.USLT || // ID3v2标签
- tags.UNSYNCEDLYRICS ||
- '';
- resolve(lyricContent.trim());
- } catch (error) {
- reject(`解析失败: ${error instanceof Error ? error.message : String(error)}`);
- }
- });
- }
- /**
- * 解析音乐文件元数据(包含歌词和其他关键字段)
- * @param inputPath 文件路径
- * @returns Promise<VideoItem> 包含完整元数据的对象
- */
- async function parseAudioMetadata(inputPath: string): Promise<VideoItem> {
- return new Promise(async (resolve, reject) => {
- inputPath = FileUtil.getFilePath(inputPath)
- try {
- // 1. 执行FFprobe命令
- const commands: string[] = [
- 'ffprobe',
- '-v', 'quiet',
- '-print_format', 'json',
- '-show_format',
- '-show_streams',
- inputPath
- ];
- let outputJson = '';
- await FFmpeg.execute(commands, {
- outputCallback: (message: string) => outputJson += message,
- });
- // 2. 解析元数据
- const metadata = JSON.parse(outputJson) as FFprobeMetadata;
- const format:FFprobeFormat = metadata.format ;
- const tags = format.tags||{} ;
- // 3. 构建VideoItem基础信息
- const videoItem = new VideoItem(
- tags.title || getFileNameWithoutExtension(inputPath),
- inputPath, // id
- inputPath,
- CommonConstants.TYPE_LOCAL,
- 0,
- new Date().toISOString() // 使用当前时间作为默认创建时间
- );
- // 4. 设置关键字段
- videoItem.bit_rate = format.bit_rate || '';
- videoItem.probe_score = format.probe_score || 0;
- videoItem.year = tags.TYER || tags.date || '';
- videoItem.lyricContent =
- tags.LYRICS ??
- tags.lyrics ??
- tags.USLT ??
- tags.UNSYNCEDLYRICS ??
- '';
- // 5. 设置其他可选字段
- videoItem.artist = tags.artist || '';
- videoItem.album = tags.album || '';
- videoItem.duration = format.duration || '';
- videoItem.nb_streams = format.nb_streams;
- videoItem.nb_programs = format.nb_programs;
- resolve(videoItem);
- } catch (error) {
- reject(new Error(`元数据解析失败: ${error instanceof Error ? error.message : String(error)}`));
- }
- });
- }
- /**
- * 将比特率(bps)转换为 kbps 并格式化
- * @param bitRate 比特率字符串(如 "5644802")
- * @param decimalPlaces 保留小数位数(默认0)
- * @returns 格式化后的 kbps 字符串(如 "5644 kbps")
- */
- function formatBitrateToKbps(bitRate: string, decimalPlaces: number = 0): string {
- // 1. 转换为数字
- const bitsPerSecond = parseInt(bitRate);
- if (isNaN(bitsPerSecond) || bitsPerSecond < 0) return "0 kbps";
- // 2. 计算 kbps(1 kbps = 1000 bps)
- const kbps = bitsPerSecond / 1000;
- // 3. 格式化输出
- return `${kbps.toFixed(decimalPlaces)} kbps`;
- }
- // 定义音质等级
- export enum AudioQuality {
- LQ = "LQ", // 低音质
- HQ = "HQ", // 高音质
- SQ = "SQ", // 无损
- HIRES = "Hi-Res", // 高解析度
- HR = "HR" // 高解析度
- }
- // 明确定义支持的音频格式类型
- interface SupportedFormats {
- LOSSY: Set<string>;
- LOSSLESS: Set<string>;
- }
- // 声明并初始化支持的音频格式(符合类型定义)
- const SUPPORTED_FORMATS: SupportedFormats = {
- LOSSY: new Set(['mp3', 'aac', 'ogg', 'opus', 'wma']),
- LOSSLESS: new Set(['flac', 'alac', 'ape', 'wav', 'aiff', 'dsf','aif'])
- };
- /**
- * 根据音频参数判断音质等级
- * @param format 编码格式(字符串,不区分大小写,如"MP3"或"flac")
- * @param bitrate 比特率(单位:bps,如320kbps需传入320000)
- * @param sampleRate 采样率(单位:Hz,如44.1kHz需传入44100)
- * @param bitDepth 位深(单位:bit,默认16bit,Hi-Res需≥24bit)
- * @returns AudioQuality 音质等级
- */
- function determineAudioQuality(
- format: string,
- bitrate: number,
- sampleRate: number,
- ): AudioQuality {
- // 统一转为小写便于比较
- const normalizedFormat = format.toLowerCase();
- // --------------- 第一步:判断是否无损/Hi-Res ---------------
- const isLossless = SUPPORTED_FORMATS.LOSSLESS.has(normalizedFormat);
- if (isLossless) {
- // Hi-Res标准:采样率≥96kHz且位深≥24bit
- if (sampleRate >= 48000 ) {
- return AudioQuality.HIRES;
- }
- // CD级无损标准:采样率≥44.1kHz且位深≥16bit
- if (sampleRate >= 44100) {
- return AudioQuality.SQ;
- }
- }
- // --------------- 第二步:有损音质判断 ---------------
- // 高音质标准:比特率≥256kbps且采样率≥44.1kHz
- if (bitrate >= 256000 && sampleRate >= 44100) {
- return AudioQuality.HQ;
- }
- // 其余情况为低音质
- else {
- return AudioQuality.LQ;
- }
- }
- function getFileFormatByPath(filePath: string): string {
- // 检查文件路径是否有效
- if (!filePath || filePath.length === 0) {
- console.error(' 文件路径不能为空');
- return '';
- }
- // 获取最后一个点号的位置
- const lastDotIndex = filePath.lastIndexOf('.');
- // 如果没有点号或者点号在最后一位,返回空字符串
- if (lastDotIndex === -1 || lastDotIndex === filePath.length - 1) {
- return '';
- }
- // 返回点号后的部分(转换为小写)
- return filePath.substring(lastDotIndex + 1).toLowerCase();
- }
- // 检查乱码的简单方法
- function hasGarbledText(tags: FFMpegTags): boolean {
- if (!tags) return false;
- const text = [tags.title, tags.artist, tags.album].join("");
- return (
- /[À-ÿµØÇò]/.test(text) || // 拉丁乱码
- /[\uFFFD�]/.test(text) || // Unicode替换字符
- /[�□▪▫▢▣▤▥▦▧▨▩▪▫◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯]/.test(text) || // 几何图形
- /[\uD800-\uDFFF]/.test(text) // 无效的UTF-16代理对
- );
- }
- /**
- * 提取华为接口的媒体文件元数据
- * @param filePath 文件路径(如:/data/storage/el1/base/test.mp3 )
- * @returns Promise<FFMpegTags> 包含专辑、艺术家、标题的元数据对象
- */
- export async function extractHwMediaMetadata(filePath: string): Promise<FFMpegTags> {
- const tags: FFMpegTags = {};
- if (!canIUse("SystemCapability.Multimedia.Media.AVMetadataExtractor")) {
- console.warn("Metadata extraction not supported on this device");
- return tags;
- }
- try {
- const avMetadataExtractor: media.AVMetadataExtractor = await media.createAVMetadataExtractor();
- const fd = await fs.open(filePath, fs.OpenMode.READ_ONLY);
- // 设置文件描述符并提取元数据
- avMetadataExtractor.fdSrc = fd;
- const metadata = await avMetadataExtractor.fetchMetadata();
- // 映射标准FFMpeg标签
- if (metadata) {
- tags.album = metadata.album ?? undefined;
- tags.artist = metadata.artist ?? metadata.author ?? undefined; // 兼容不同元数据字段
- tags.title = metadata.title ?? undefined;
- }
- // 关闭文件描述符
- await fs.close(fd);
- return tags;
- } catch (error) {
- const err = error as BusinessError;
- console.error(`Metadata extraction failed: ${err.code}, ${err.message}`);
- return tags;
- }
- return tags;
- }
|