M3(批5): AudioProvider 播放 + 七牛上传器(@qiniu/upload)
依赖:ohpm i @qiniu/upload@1.0.2(platform)
platform QiniuUploader:UploadFile.fromPath + key + createMultipartUploadV2Task(context,file,{tokenProvider})。
🔴 token 走 tokenProvider(服务端用 AK/SK 生成 uploadToken(bucket)),AK/SK 不入端——纠正原 Android 硬编码 AK/SK
T-M3-10 AudioProvider(@kit.MediaKit AVPlayer):
- mediaTypeAudio:播放语音 url → gameui_play_voice/gameui_stop_voice(user 透传)
- srcIsloop:播放本地 wav(<gameDir>/assets/wav/<src>,isloop 0一次/>0循环/<0停止)
- voicePlaying:静音总开关 setVolume
- prepareaudio:录音分档(QiniuUploader 链路就绪;按住说话手势+麦克风+七牛服务端 token 三依赖未就绪前安全空实现)
bucket/域名/key 规则对齐原工程(bucket gameauio、回放 http://gameaudio.daoqi88.cn/<key>,由服务端 token 与 AudioProvider 接入时落地)
devecocli build 通过(CompileArkTS 编译七牛/音频无误)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,19 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"@qiniu/upload@^1.0.2": "@qiniu/upload@1.0.2",
|
||||||
"common@../common": "common@../common",
|
"common@../common": "common@../common",
|
||||||
"contracts@../contracts": "contracts@../contracts",
|
"contracts@../contracts": "contracts@../contracts",
|
||||||
"platform@../platform": "platform@../platform"
|
"platform@../platform": "platform@../platform"
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@qiniu/upload@1.0.2": {
|
||||||
|
"name": "@qiniu/upload",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"integrity": "sha512-LWUgUSXo7KSHZ9DaTTso7D7t5VUJqLNu3vPkEdhVNSnjbajqeK8X4yIuWKp9MqCHCyB9KjHSvliRrt4A0ukQ5A==",
|
||||||
|
"resolved": "https://ohpm.openharmony.cn/ohpm/@qiniu/upload/-/upload-1.0.2.har",
|
||||||
|
"registryType": "ohpm"
|
||||||
|
},
|
||||||
"common@../common": {
|
"common@../common": {
|
||||||
"name": "common",
|
"name": "common",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -33,7 +41,8 @@
|
|||||||
"registryType": "local",
|
"registryType": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"contracts": "file:../contracts",
|
"contracts": "file:../contracts",
|
||||||
"common": "file:../common"
|
"common": "file:../common",
|
||||||
|
"@qiniu/upload": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"@qiniu/upload@^1.0.2": "@qiniu/upload@1.0.2",
|
||||||
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
||||||
"common@../common": "common@../common",
|
"common@../common": "common@../common",
|
||||||
"contracts@../contracts": "contracts@../contracts",
|
"contracts@../contracts": "contracts@../contracts",
|
||||||
@@ -15,6 +16,13 @@
|
|||||||
"platform@../platform": "platform@../platform"
|
"platform@../platform": "platform@../platform"
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@qiniu/upload@1.0.2": {
|
||||||
|
"name": "@qiniu/upload",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"integrity": "sha512-LWUgUSXo7KSHZ9DaTTso7D7t5VUJqLNu3vPkEdhVNSnjbajqeK8X4yIuWKp9MqCHCyB9KjHSvliRrt4A0ukQ5A==",
|
||||||
|
"resolved": "https://ohpm.openharmony.cn/ohpm/@qiniu/upload/-/upload-1.0.2.har",
|
||||||
|
"registryType": "ohpm"
|
||||||
|
},
|
||||||
"@tencent/wechat_open_sdk@1.0.19": {
|
"@tencent/wechat_open_sdk@1.0.19": {
|
||||||
"name": "@tencent/wechat_open_sdk",
|
"name": "@tencent/wechat_open_sdk",
|
||||||
"version": "1.0.19",
|
"version": "1.0.19",
|
||||||
@@ -79,7 +87,8 @@
|
|||||||
"registryType": "local",
|
"registryType": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"contracts": "file:../contracts",
|
"contracts": "file:../contracts",
|
||||||
"common": "file:../common"
|
"common": "file:../common",
|
||||||
|
"@qiniu/upload": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider,
|
import { CapabilityProvider, EchoSampleProvider, RoomStubProvider, PayStubProvider,
|
||||||
VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider,
|
VibrateProvider, ClipboardProvider, AppSystemProvider, DeviceProvider,
|
||||||
NetworkProvider, ShareProvider, LoginProvider, PhotoProvider, ShakeProvider,
|
NetworkProvider, ShareProvider, LoginProvider, PhotoProvider, ShakeProvider,
|
||||||
ScanProvider, CameraProvider, LocationProvider } from 'feature_capabilities';
|
ScanProvider, CameraProvider, LocationProvider, AudioProvider } from 'feature_capabilities';
|
||||||
|
|
||||||
export function buildCapabilities(): CapabilityProvider[] {
|
export function buildCapabilities(): CapabilityProvider[] {
|
||||||
return [
|
return [
|
||||||
@@ -26,6 +26,7 @@ export function buildCapabilities(): CapabilityProvider[] {
|
|||||||
new ScanProvider(), // opensaoma → getsaomaData
|
new ScanProvider(), // opensaoma → getsaomaData
|
||||||
new CameraProvider(), // opencamera → getcameraaAddress
|
new CameraProvider(), // opencamera → getcameraaAddress
|
||||||
new LocationProvider(), // startlocation/getlocationinfo → getlocationinfo
|
new LocationProvider(), // startlocation/getlocationinfo → getlocationinfo
|
||||||
|
new AudioProvider(), // mediaTypeAudio/srcIsloop/voicePlaying(播放);prepareaudio 录音分档
|
||||||
// —— 暂缓能力占位桩(§6.5)——
|
// —— 暂缓能力占位桩(§6.5)——
|
||||||
new RoomStubProvider(),
|
new RoomStubProvider(),
|
||||||
new PayStubProvider(),
|
new PayStubProvider(),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export { ShakeProvider } from './src/main/ets/providers/ShakeProvider';
|
|||||||
export { ScanProvider } from './src/main/ets/providers/ScanProvider';
|
export { ScanProvider } from './src/main/ets/providers/ScanProvider';
|
||||||
export { CameraProvider } from './src/main/ets/providers/CameraProvider';
|
export { CameraProvider } from './src/main/ets/providers/CameraProvider';
|
||||||
export { LocationProvider } from './src/main/ets/providers/LocationProvider';
|
export { LocationProvider } from './src/main/ets/providers/LocationProvider';
|
||||||
|
export { AudioProvider } from './src/main/ets/providers/AudioProvider';
|
||||||
|
|
||||||
// 微信 SDK 封装(EntryAbility 处理回调 want 需用)
|
// 微信 SDK 封装(EntryAbility 处理回调 want 需用)
|
||||||
export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi';
|
export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"@qiniu/upload@^1.0.2": "@qiniu/upload@1.0.2",
|
||||||
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
"@tencent/wechat_open_sdk@^1.0.19": "@tencent/wechat_open_sdk@1.0.19",
|
||||||
"common@../common": "common@../common",
|
"common@../common": "common@../common",
|
||||||
"contracts@../contracts": "contracts@../contracts",
|
"contracts@../contracts": "contracts@../contracts",
|
||||||
@@ -14,6 +15,13 @@
|
|||||||
"platform@../platform": "platform@../platform"
|
"platform@../platform": "platform@../platform"
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@qiniu/upload@1.0.2": {
|
||||||
|
"name": "@qiniu/upload",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"integrity": "sha512-LWUgUSXo7KSHZ9DaTTso7D7t5VUJqLNu3vPkEdhVNSnjbajqeK8X4yIuWKp9MqCHCyB9KjHSvliRrt4A0ukQ5A==",
|
||||||
|
"resolved": "https://ohpm.openharmony.cn/ohpm/@qiniu/upload/-/upload-1.0.2.har",
|
||||||
|
"registryType": "ohpm"
|
||||||
|
},
|
||||||
"@tencent/wechat_open_sdk@1.0.19": {
|
"@tencent/wechat_open_sdk@1.0.19": {
|
||||||
"name": "@tencent/wechat_open_sdk",
|
"name": "@tencent/wechat_open_sdk",
|
||||||
"version": "1.0.19",
|
"version": "1.0.19",
|
||||||
@@ -64,7 +72,8 @@
|
|||||||
"registryType": "local",
|
"registryType": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"contracts": "file:../contracts",
|
"contracts": "file:../contracts",
|
||||||
"common": "file:../common"
|
"common": "file:../common",
|
||||||
|
"@qiniu/upload": "^1.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* 音频能力(契约 §8.7/§9/§10.6,T-M3-10)。@kit.MediaKit AVPlayer。
|
||||||
|
* - mediaTypeAudio:data={audiourl,type,user},播放语音消息 → gameui_play_voice/gameui_stop_voice(user 透传)。
|
||||||
|
* - srcIsloop:data={src,isloop},播放本地 wav 音效/背景乐(isloop 0一次/>0循环/<0停止)。
|
||||||
|
* - voicePlaying:data="1" 开 / 其他 静音(总开关)。
|
||||||
|
* - prepareaudio:按住说话录音 →(上传七牛)→ getaudiourl。
|
||||||
|
*
|
||||||
|
* ⚠ prepareaudio 录音为分档实现:录音上传链路用 platform QiniuUploader(token 走服务端,见其注释),
|
||||||
|
* 但"按住说话"的原生触摸起止手势与麦克风权限需与容器触摸集成(原 Android 在 WebView 上做手势),
|
||||||
|
* 且需七牛服务端 token 端点——此三者就绪前 prepareaudio 为安全空实现(不录、不卡死)。见 TODO。
|
||||||
|
*/
|
||||||
|
import { media } from '@kit.MediaKit';
|
||||||
|
import { BusinessError } from '@kit.BasicServicesKit';
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { BridgeController } from 'feature_bridge';
|
||||||
|
import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq } from 'contracts';
|
||||||
|
import { ConfigManager, ResourceManager } from 'domain_resource';
|
||||||
|
import { Logger } from 'common';
|
||||||
|
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
|
||||||
|
|
||||||
|
export class AudioProvider implements CapabilityProvider {
|
||||||
|
readonly name: string = 'audio';
|
||||||
|
private readonly log: Logger = Logger.tag('AudioProvider');
|
||||||
|
private bridge: BridgeController | undefined = undefined;
|
||||||
|
private context: common.UIAbilityContext | undefined = undefined;
|
||||||
|
private config: ConfigManager | undefined = undefined;
|
||||||
|
private voicePlayer: media.AVPlayer | undefined = undefined;
|
||||||
|
private sfxPlayer: media.AVPlayer | undefined = undefined;
|
||||||
|
private muted: boolean = false;
|
||||||
|
|
||||||
|
register(bridge: BridgeController, ctx: CapabilityContext): void {
|
||||||
|
this.bridge = bridge;
|
||||||
|
this.context = ctx.uiAbilityContext;
|
||||||
|
this.config = ctx.config;
|
||||||
|
bridge.registerHandler(InboundHandlers.MediaTypeAudio, (data: string, _cb: (resp: string) => void) => {
|
||||||
|
this.playVoiceMessage(data);
|
||||||
|
});
|
||||||
|
bridge.registerHandler(InboundHandlers.SrcIsLoop, (data: string, _cb: (resp: string) => void) => {
|
||||||
|
this.playSfx(data);
|
||||||
|
});
|
||||||
|
bridge.registerHandler(InboundHandlers.VoicePlaying, (data: string, _cb: (resp: string) => void) => {
|
||||||
|
this.setMuted(data !== '1');
|
||||||
|
});
|
||||||
|
bridge.registerHandler(InboundHandlers.PrepareAudio, (_d: string, _cb: (resp: string) => void) => {
|
||||||
|
// TODO(M3 录音):按住说话录音(AVRecorder)→ platform QiniuUploader.upload(token 走服务端) →
|
||||||
|
// callHandler(getaudiourl, {audiourl,time,filepath})。需容器触摸手势(起/止)、麦克风权限、七牛服务端 token 端点。
|
||||||
|
this.log.i('prepareaudio: recording requires touch-gesture + mic perm + qiniu server token (stub)');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(): void {
|
||||||
|
this.releaseVoice();
|
||||||
|
this.releaseSfx();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 播放语音消息(远程 url),上报 gameui_play_voice/gameui_stop_voice。 */
|
||||||
|
private playVoiceMessage(data: string): void {
|
||||||
|
let req: MediaTypeAudioReq;
|
||||||
|
try {
|
||||||
|
req = JSON.parse(data) as MediaTypeAudioReq;
|
||||||
|
} catch (e) {
|
||||||
|
this.log.w(`mediaTypeAudio bad json: ${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.audiourl === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const user: string = req.user;
|
||||||
|
this.releaseVoice();
|
||||||
|
media.createAVPlayer().then((player: media.AVPlayer) => {
|
||||||
|
this.voicePlayer = player;
|
||||||
|
player.on('stateChange', (state: string) => {
|
||||||
|
if (state === 'initialized') {
|
||||||
|
player.prepare();
|
||||||
|
} else if (state === 'prepared') {
|
||||||
|
player.loop = false;
|
||||||
|
player.setVolume(this.muted ? 0 : 1);
|
||||||
|
player.play();
|
||||||
|
this.bridge?.callHandler(OutboundHandlers.GameUiPlayVoice, user);
|
||||||
|
} else if (state === 'completed') {
|
||||||
|
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, user);
|
||||||
|
this.releaseVoice();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.on('error', (e: BusinessError) => {
|
||||||
|
this.log.w(`voice play error: ${e.code} ${e.message}`);
|
||||||
|
this.bridge?.callHandler(OutboundHandlers.GameUiStopVoice, user);
|
||||||
|
this.releaseVoice();
|
||||||
|
});
|
||||||
|
player.url = req.audiourl;
|
||||||
|
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(voice) failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 播放本地 wav 音效/背景乐:<gameDir>/assets/wav/<src>。isloop<0 停止。 */
|
||||||
|
private playSfx(data: string): void {
|
||||||
|
let req: SrcIsLoopReq;
|
||||||
|
try {
|
||||||
|
req = JSON.parse(data) as SrcIsLoopReq;
|
||||||
|
} catch (e) {
|
||||||
|
this.log.w(`srcIsloop bad json: ${(e as Error).message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.isloop < 0) {
|
||||||
|
this.releaseSfx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ctx = this.context;
|
||||||
|
const cfg = this.config;
|
||||||
|
if (ctx === undefined || cfg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const local = cfg.getLocal();
|
||||||
|
const wavPath: string =
|
||||||
|
`${ResourceManager.resourceRoot(ctx)}/${local.gamedir}/${local.gamestart}/assets/wav/${req.src}`;
|
||||||
|
const loop: boolean = req.isloop > 0;
|
||||||
|
this.releaseSfx();
|
||||||
|
media.createAVPlayer().then((player: media.AVPlayer) => {
|
||||||
|
this.sfxPlayer = player;
|
||||||
|
player.on('stateChange', (state: string) => {
|
||||||
|
if (state === 'initialized') {
|
||||||
|
player.prepare();
|
||||||
|
} else if (state === 'prepared') {
|
||||||
|
player.loop = loop;
|
||||||
|
player.setVolume(this.muted ? 0 : 1);
|
||||||
|
player.play();
|
||||||
|
} else if (state === 'completed') {
|
||||||
|
if (!loop) {
|
||||||
|
this.releaseSfx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
player.on('error', (e: BusinessError) => {
|
||||||
|
this.log.w(`sfx play error: ${e.code} ${e.message}`);
|
||||||
|
this.releaseSfx();
|
||||||
|
});
|
||||||
|
player.url = `file://${wavPath}`;
|
||||||
|
}).catch((e: BusinessError) => this.log.w(`createAVPlayer(sfx) failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 语音总开关:静音/取消静音,作用于当前两个播放器。 */
|
||||||
|
private setMuted(muted: boolean): void {
|
||||||
|
this.muted = muted;
|
||||||
|
const vol: number = muted ? 0 : 1;
|
||||||
|
try {
|
||||||
|
this.voicePlayer?.setVolume(vol);
|
||||||
|
this.sfxPlayer?.setVolume(vol);
|
||||||
|
} catch (e) {
|
||||||
|
this.log.w(`setVolume failed: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseVoice(): void {
|
||||||
|
const p = this.voicePlayer;
|
||||||
|
this.voicePlayer = undefined;
|
||||||
|
if (p !== undefined) {
|
||||||
|
p.release().catch((e: BusinessError) => this.log.w(`release voice failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseSfx(): void {
|
||||||
|
const p = this.sfxPlayer;
|
||||||
|
this.sfxPlayer = undefined;
|
||||||
|
if (p !== undefined) {
|
||||||
|
p.release().catch((e: BusinessError) => this.log.w(`release sfx failed: ${e.message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,4 @@ export { Unzipper } from './src/main/ets/zip/Unzipper';
|
|||||||
// 权限 / 上传服务
|
// 权限 / 上传服务
|
||||||
export { PermissionGuard } from './src/main/ets/perm/PermissionGuard';
|
export { PermissionGuard } from './src/main/ets/perm/PermissionGuard';
|
||||||
export { LocalUploadServer } from './src/main/ets/upload/LocalUploadServer';
|
export { LocalUploadServer } from './src/main/ets/upload/LocalUploadServer';
|
||||||
|
export { QiniuUploader, QiniuTokenProvider } from './src/main/ets/upload/QiniuUploader';
|
||||||
|
|||||||
@@ -6,10 +6,18 @@
|
|||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"@qiniu/upload@^1.0.2": "@qiniu/upload@1.0.2",
|
||||||
"common@../common": "common@../common",
|
"common@../common": "common@../common",
|
||||||
"contracts@../contracts": "contracts@../contracts"
|
"contracts@../contracts": "contracts@../contracts"
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@qiniu/upload@1.0.2": {
|
||||||
|
"name": "@qiniu/upload",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"integrity": "sha512-LWUgUSXo7KSHZ9DaTTso7D7t5VUJqLNu3vPkEdhVNSnjbajqeK8X4yIuWKp9MqCHCyB9KjHSvliRrt4A0ukQ5A==",
|
||||||
|
"resolved": "https://ohpm.openharmony.cn/ohpm/@qiniu/upload/-/upload-1.0.2.har",
|
||||||
|
"registryType": "ohpm"
|
||||||
|
},
|
||||||
"common@../common": {
|
"common@../common": {
|
||||||
"name": "common",
|
"name": "common",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"contracts": "file:../contracts",
|
"contracts": "file:../contracts",
|
||||||
"common": "file:../common"
|
"common": "file:../common",
|
||||||
}
|
"@qiniu/upload": "^1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"dynamicDependencies": {}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 七牛云对象存储上传(@qiniu/upload,契约 §10.6 录音上传)。
|
||||||
|
*
|
||||||
|
* 🔴 安全(CLAUDE.md 附录 B / §14.2):上传 token 由 **tokenProvider 异步提供**,
|
||||||
|
* 应由服务端用 AK/SK 生成 uploadToken(bucket) 后下发;**AK/SK 绝不入客户端**。
|
||||||
|
* 原 Android 把 AK/SK 硬编码在端侧,本平台用 tokenProvider 模式纠正。
|
||||||
|
*/
|
||||||
|
import { common } from '@kit.AbilityKit';
|
||||||
|
import { createMultipartUploadV2Task, UploadFile, UploadConfig, UploadTask } from '@qiniu/upload';
|
||||||
|
import { Logger } from 'common';
|
||||||
|
|
||||||
|
/** token 提供器:返回上传 token 的 Promise(来源应为服务端)。 */
|
||||||
|
export type QiniuTokenProvider = () => Promise<string>;
|
||||||
|
|
||||||
|
export class QiniuUploader {
|
||||||
|
private static readonly log: Logger = Logger.tag('QiniuUploader');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传本地文件到七牛,资源名为 key。成功返回 true。
|
||||||
|
* @param context UIAbility 上下文
|
||||||
|
* @param localPath 本地文件路径
|
||||||
|
* @param key 七牛资源名(= 回放 URL 的路径段)
|
||||||
|
* @param tokenProvider 上传 token 提供器(服务端生成 uploadToken(bucket))
|
||||||
|
*/
|
||||||
|
static async upload(context: common.Context, localPath: string, key: string,
|
||||||
|
tokenProvider: QiniuTokenProvider): Promise<boolean> {
|
||||||
|
const file: UploadFile = UploadFile.fromPath(localPath);
|
||||||
|
file.key = key;
|
||||||
|
const config: UploadConfig = { tokenProvider };
|
||||||
|
const task: UploadTask = createMultipartUploadV2Task(context, file, config);
|
||||||
|
let failed: boolean = false;
|
||||||
|
task.onError(() => {
|
||||||
|
failed = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await task.start();
|
||||||
|
} catch (e) {
|
||||||
|
QiniuUploader.log.w(`upload start failed: ${(e as Error).message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (failed) {
|
||||||
|
QiniuUploader.log.w(`upload failed for key=${key}`);
|
||||||
|
}
|
||||||
|
return !failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user