ref |> 处理播控中心不生效问题
Signed-off-by: xugenyuan <xugenyuan@wondertek.com.cn>
Showing
5 changed files
with
103 additions
and
47 deletions
| @@ -372,7 +372,7 @@ export struct OperRowListView { | @@ -372,7 +372,7 @@ export struct OperRowListView { | ||
| 372 | .gesture( | 372 | .gesture( |
| 373 | TapGesture() | 373 | TapGesture() |
| 374 | .onAction((event: GestureEvent) => { | 374 | .onAction((event: GestureEvent) => { |
| 375 | - this.AudioSuspension.setPlayerUrl(this.audioUrl, this.audioTitle) | 375 | + this.AudioSuspension.setPlayerUrl(this.audioUrl, this.audioTitle, this.contentDetailData.newsId + "", this.contentDetailData.newsSourceName) |
| 376 | TrackingButton.click('suspendedWindow',this.pageId,this.pageName) | 376 | TrackingButton.click('suspendedWindow',this.pageId,this.pageName) |
| 377 | })) | 377 | })) |
| 378 | } | 378 | } |
| @@ -39,14 +39,15 @@ export class AudioSuspensionModel { | @@ -39,14 +39,15 @@ export class AudioSuspensionModel { | ||
| 39 | /** | 39 | /** |
| 40 | * 配置音频地址 | 40 | * 配置音频地址 |
| 41 | */ | 41 | */ |
| 42 | - public setPlayerUrl(url: string, srcTitle: string) { | 42 | + public async setPlayerUrl(url: string, srcTitle: string, srcContentId?: string, srcSource?: string) { |
| 43 | /*console.log(TAG,'this.url', this.url) | 43 | /*console.log(TAG,'this.url', this.url) |
| 44 | console.log(TAG,'url', url)*/ | 44 | console.log(TAG,'url', url)*/ |
| 45 | this.playerController.get().keepOnBackground = true | 45 | this.playerController.get().keepOnBackground = true |
| 46 | BackgroundAudioController.sharedController().avplayerController = this.playerController.get() | 46 | BackgroundAudioController.sharedController().avplayerController = this.playerController.get() |
| 47 | - BackgroundAudioController.sharedController().createSession() | 47 | + await BackgroundAudioController.sharedController().createSession() |
| 48 | BackgroundAudioController.sharedController().startContinuousTask() | 48 | BackgroundAudioController.sharedController().startContinuousTask() |
| 49 | - BackgroundAudioController.sharedController().setSessionMetaData("aaa", srcTitle, "") | 49 | + let id = $r('app.media.newspaper_default').id |
| 50 | + BackgroundAudioController.sharedController().setSessionMetaData(srcContentId ?? "", srcTitle, 'file://', srcSource ?? "") | ||
| 50 | BackgroundAudioController.sharedController().stopUseFeatures() | 51 | BackgroundAudioController.sharedController().stopUseFeatures() |
| 51 | BackgroundAudioController.sharedController().listenPlayEvents() | 52 | BackgroundAudioController.sharedController().listenPlayEvents() |
| 52 | 53 |
| @@ -116,6 +116,7 @@ export class LiveRoomManager { | @@ -116,6 +116,7 @@ export class LiveRoomManager { | ||
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | private connectWithIMToken(token: string) { | 118 | private connectWithIMToken(token: string) { |
| 119 | + Logger.debug(TAG, "连接开始,IM Token:" + token) | ||
| 119 | const timeout = 5 | 120 | const timeout = 5 |
| 120 | IMEngine.getInstance().connect(token, timeout).then(result => { | 121 | IMEngine.getInstance().connect(token, timeout).then(result => { |
| 121 | if (EngineError.Success === result.code) { | 122 | if (EngineError.Success === result.code) { |
| @@ -5,6 +5,7 @@ import { BusinessError } from '@kit.BasicServicesKit' | @@ -5,6 +5,7 @@ import { BusinessError } from '@kit.BasicServicesKit' | ||
| 5 | import { Logger } from 'wdKit/Index' | 5 | import { Logger } from 'wdKit/Index' |
| 6 | import { PlayerConstants } from '../constants/PlayerConstants' | 6 | import { PlayerConstants } from '../constants/PlayerConstants' |
| 7 | import { WDPlayerController } from './WDPlayerController' | 7 | import { WDPlayerController } from './WDPlayerController' |
| 8 | +import { image } from '@kit.ImageKit' | ||
| 8 | 9 | ||
| 9 | const TAG = "BackgroundAudioController" | 10 | const TAG = "BackgroundAudioController" |
| 10 | 11 | ||
| @@ -23,20 +24,28 @@ export class BackgroundAudioController { | @@ -23,20 +24,28 @@ export class BackgroundAudioController { | ||
| 23 | public gotContextFunc?: () => Context | 24 | public gotContextFunc?: () => Context |
| 24 | public avplayerController?: WDPlayerController | 25 | public avplayerController?: WDPlayerController |
| 25 | private lastSession?: AVSessionManager.AVSession | 26 | private lastSession?: AVSessionManager.AVSession |
| 27 | + private applyedLongTaskPlay: boolean = false | ||
| 28 | + private lastProgress: number = 0.0 | ||
| 29 | + private hasSetupProgress: boolean = false | ||
| 26 | 30 | ||
| 27 | // 开始创建并激活媒体会话 | 31 | // 开始创建并激活媒体会话 |
| 28 | // 创建session | 32 | // 创建session |
| 29 | async createSession() { | 33 | async createSession() { |
| 30 | if (!this.gotContextFunc) { return } | 34 | if (!this.gotContextFunc) { return } |
| 31 | - this.destorySession() | ||
| 32 | - let type: AVSessionManager.AVSessionType = 'audio'; | ||
| 33 | - let session = await AVSessionManager.createAVSession(this.gotContextFunc(),'SESSION_NAME', type); | 35 | + |
| 36 | + if (this.lastSession == null) { | ||
| 37 | + this.destorySession() | ||
| 38 | + let type: AVSessionManager.AVSessionType = 'audio'; | ||
| 39 | + let session = await AVSessionManager.createAVSession(this.gotContextFunc(),'SESSION_NAME', type); | ||
| 40 | + this.lastSession = session | ||
| 41 | + } | ||
| 34 | 42 | ||
| 35 | // 激活接口要在元数据、控制命令注册完成之后再执行 | 43 | // 激活接口要在元数据、控制命令注册完成之后再执行 |
| 36 | - await session.activate(); | ||
| 37 | - Logger.debug(TAG, `session create done : sessionId : ${session.sessionId}`); | 44 | + await this.lastSession?.activate(); |
| 45 | + Logger.debug(TAG, `session create done : sessionId : ${this.lastSession?.sessionId}`); | ||
| 38 | 46 | ||
| 39 | - this.lastSession = session | 47 | + this.lastProgress = 0 |
| 48 | + this.hasSetupProgress = false | ||
| 40 | } | 49 | } |
| 41 | 50 | ||
| 42 | destorySession() { | 51 | destorySession() { |
| @@ -47,13 +56,13 @@ export class BackgroundAudioController { | @@ -47,13 +56,13 @@ export class BackgroundAudioController { | ||
| 47 | } | 56 | } |
| 48 | 57 | ||
| 49 | //设置播放元数据 | 58 | //设置播放元数据 |
| 50 | - setSessionMetaData(assetId: string, title: string, mediaImage: string, artist?: string) { | 59 | + setSessionMetaData(assetId: string, title: string, mediaImage: image.PixelMap | string, artist: string) { |
| 60 | + Logger.debug(TAG, `SetAVMetadata assetId: ${assetId}}, title: ${title}, mediaImage: ${mediaImage}, artist: ${artist}`); | ||
| 51 | let metadata: AVSessionManager.AVMetadata = { | 61 | let metadata: AVSessionManager.AVMetadata = { |
| 52 | - assetId: assetId, | ||
| 53 | - title: title, | 62 | + assetId: assetId.length > 0 ? assetId : "fake-asset-id", |
| 63 | + title: title.length > 0 ? title : " ", | ||
| 54 | mediaImage: mediaImage, | 64 | mediaImage: mediaImage, |
| 55 | - artist: artist ?? "", | ||
| 56 | - // duration: "", | 65 | + artist: artist.length > 0 ? artist : "人日日报", |
| 57 | }; | 66 | }; |
| 58 | this.lastSession?.setAVMetadata(metadata).then(() => { | 67 | this.lastSession?.setAVMetadata(metadata).then(() => { |
| 59 | Logger.debug(TAG, `SetAVMetadata successfully`); | 68 | Logger.debug(TAG, `SetAVMetadata successfully`); |
| @@ -64,26 +73,59 @@ export class BackgroundAudioController { | @@ -64,26 +73,59 @@ export class BackgroundAudioController { | ||
| 64 | 73 | ||
| 65 | //设置播放状态 | 74 | //设置播放状态 |
| 66 | setSessionPlayStatus(playStatus: number) { | 75 | setSessionPlayStatus(playStatus: number) { |
| 67 | - let pauseed = playStatus == PlayerConstants.STATUS_PAUSE | 76 | + let playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY |
| 77 | + switch (playStatus){ | ||
| 78 | + case PlayerConstants.STATUS_PAUSE: { | ||
| 79 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE | ||
| 80 | + } break | ||
| 81 | + case PlayerConstants.STATUS_START: { | ||
| 82 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY | ||
| 83 | + } break | ||
| 84 | + case PlayerConstants.STATUS_STOP: { | ||
| 85 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_STOP | ||
| 86 | + } break | ||
| 87 | + case PlayerConstants.STATUS_ERROR: { | ||
| 88 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_ERROR | ||
| 89 | + } break | ||
| 90 | + case PlayerConstants.STATUS_COMPLETION: { | ||
| 91 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_COMPLETED | ||
| 92 | + } break | ||
| 93 | + default: { | ||
| 94 | + playbackStatus = AVSessionManager.PlaybackState.PLAYBACK_STATE_IDLE | ||
| 95 | + } break | ||
| 96 | + } | ||
| 68 | let playbackState: AVSessionManager.AVPlaybackState = { | 97 | let playbackState: AVSessionManager.AVPlaybackState = { |
| 69 | - state:pauseed ? AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE : AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY, | ||
| 70 | - isFavorite:false | 98 | + state:playbackStatus, |
| 99 | + // isFavorite:false | ||
| 71 | }; | 100 | }; |
| 72 | this.lastSession?.setAVPlaybackState(playbackState, (err: BusinessError) => { | 101 | this.lastSession?.setAVPlaybackState(playbackState, (err: BusinessError) => { |
| 73 | if (err) { | 102 | if (err) { |
| 74 | Logger.error(TAG, `Failed to set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); | 103 | Logger.error(TAG, `Failed to set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); |
| 75 | } else { | 104 | } else { |
| 76 | - Logger.debug(TAG, `SetAVPlaybackState 设置播放状态成功` + playStatus); | 105 | + Logger.debug(TAG, `SetAVPlaybackState 设置播放状态成功 ` + playStatus); |
| 77 | } | 106 | } |
| 78 | }); | 107 | }); |
| 79 | } | 108 | } |
| 80 | 109 | ||
| 81 | //设置进度,单位秒 | 110 | //设置进度,单位秒 |
| 82 | setSessionPlayProgress(progressDuration: number, totalDuration: number) { | 111 | setSessionPlayProgress(progressDuration: number, totalDuration: number) { |
| 83 | - Logger.debug(TAG, `set progress: ` + progressDuration + " duration: " + totalDuration); | 112 | + // Logger.debug(TAG, `set progress: ` + progressDuration + " duration: " + totalDuration); |
| 84 | if (totalDuration <= 0) { | 113 | if (totalDuration <= 0) { |
| 85 | return | 114 | return |
| 86 | } | 115 | } |
| 116 | + let newProgress = progressDuration / totalDuration | ||
| 117 | + if (Math.abs(newProgress - this.lastProgress) < 0.01) { | ||
| 118 | + return | ||
| 119 | + } | ||
| 120 | + this.lastProgress = newProgress | ||
| 121 | + | ||
| 122 | + if (this.hasSetupProgress) { | ||
| 123 | + return | ||
| 124 | + } | ||
| 125 | + this.hasSetupProgress = true | ||
| 126 | + | ||
| 127 | + Logger.debug(TAG, `set progress: ` + progressDuration + " duration: " + totalDuration); | ||
| 128 | + | ||
| 87 | // 设置状态: 播放状态,进度位置,播放倍速,缓存的时间 | 129 | // 设置状态: 播放状态,进度位置,播放倍速,缓存的时间 |
| 88 | let playbackState: AVSessionManager.AVPlaybackState = { | 130 | let playbackState: AVSessionManager.AVPlaybackState = { |
| 89 | state: AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY, // 播放状态 | 131 | state: AVSessionManager.PlaybackState.PLAYBACK_STATE_PLAY, // 播放状态 |
| @@ -92,14 +134,14 @@ export class BackgroundAudioController { | @@ -92,14 +134,14 @@ export class BackgroundAudioController { | ||
| 92 | updateTime: new Date().getTime(), // 应用更新当前位置时的时间戳,以ms为单位 | 134 | updateTime: new Date().getTime(), // 应用更新当前位置时的时间戳,以ms为单位 |
| 93 | }, | 135 | }, |
| 94 | duration: totalDuration * 1000, | 136 | duration: totalDuration * 1000, |
| 95 | - // speed: 1.0, // 可选,默认是1.0,播放的倍速,按照应用内支持的speed进行设置,系统不做校验 | ||
| 96 | - // bufferedTime: 14000, // 可选,资源缓存的时间,以ms为单位 | 137 | + speed: 1.0, // 可选,默认是1.0,播放的倍速,按照应用内支持的speed进行设置,系统不做校验 |
| 138 | + bufferedTime: totalDuration * 1000, // 可选,资源缓存的时间,以ms为单位 | ||
| 97 | }; | 139 | }; |
| 98 | this.lastSession?.setAVPlaybackState(playbackState, (err) => { | 140 | this.lastSession?.setAVPlaybackState(playbackState, (err) => { |
| 99 | if (err) { | 141 | if (err) { |
| 100 | Logger.error(TAG, `Failed to set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); | 142 | Logger.error(TAG, `Failed to set AVPlaybackState. Code: ${err.code}, message: ${err.message}`); |
| 101 | } else { | 143 | } else { |
| 102 | - // Logger.debug(TAG, `SetAVPlaybackState successfully`); | 144 | + Logger.debug(TAG, `SetAVPlaybackState successfully`); |
| 103 | } | 145 | } |
| 104 | }); | 146 | }); |
| 105 | } | 147 | } |
| @@ -107,34 +149,37 @@ export class BackgroundAudioController { | @@ -107,34 +149,37 @@ export class BackgroundAudioController { | ||
| 107 | // 置灰或禁用不支持的按钮 | 149 | // 置灰或禁用不支持的按钮 |
| 108 | stopUseFeatures() { | 150 | stopUseFeatures() { |
| 109 | // 取消指定session下的相关监听 | 151 | // 取消指定session下的相关监听 |
| 110 | - this.lastSession?.off('playFromAssetId'); | ||
| 111 | - this.lastSession?.off('setSpeed'); | ||
| 112 | - this.lastSession?.off('setLoopMode'); | ||
| 113 | - this.lastSession?.off('toggleFavorite'); | ||
| 114 | - this.lastSession?.off('skipToQueueItem'); | ||
| 115 | - this.lastSession?.off('handleKeyEvent'); | ||
| 116 | - this.lastSession?.off('commonCommand'); | ||
| 117 | - this.lastSession?.off('rewind'); | ||
| 118 | - this.lastSession?.off('fastForward'); | 152 | + // this.lastSession?.off('playFromAssetId'); |
| 153 | + // this.lastSession?.off('setSpeed'); | ||
| 154 | + // this.lastSession?.off('setLoopMode'); | ||
| 155 | + // this.lastSession?.off('toggleFavorite'); | ||
| 156 | + // this.lastSession?.off('skipToQueueItem'); | ||
| 157 | + // this.lastSession?.off('handleKeyEvent'); | ||
| 158 | + // this.lastSession?.off('commonCommand'); | ||
| 159 | + // this.lastSession?.off('rewind'); | ||
| 160 | + // this.lastSession?.off('fastForward'); | ||
| 119 | } | 161 | } |
| 120 | 162 | ||
| 121 | listenPlayEvents() { | 163 | listenPlayEvents() { |
| 122 | this.lastSession?.on('play', () => { | 164 | this.lastSession?.on('play', () => { |
| 123 | Logger.debug(TAG, `on play `); | 165 | Logger.debug(TAG, `on play `); |
| 166 | + this.avplayerController?.play() | ||
| 167 | + this.hasSetupProgress = false | ||
| 124 | }); | 168 | }); |
| 125 | this.lastSession?.on('pause', () => { | 169 | this.lastSession?.on('pause', () => { |
| 126 | Logger.debug(TAG, `on pause `); | 170 | Logger.debug(TAG, `on pause `); |
| 171 | + this.avplayerController?.pause() | ||
| 127 | }); | 172 | }); |
| 128 | this.lastSession?.on('stop', () => { | 173 | this.lastSession?.on('stop', () => { |
| 129 | Logger.debug(TAG, `on stop `); | 174 | Logger.debug(TAG, `on stop `); |
| 175 | + this.avplayerController?.stop() | ||
| 130 | }); | 176 | }); |
| 131 | - this.lastSession?.on('playNext', () => { | ||
| 132 | - Logger.debug(TAG, `on playNext `); | ||
| 133 | - }); | ||
| 134 | - this.lastSession?.on('playPrevious', () => { | ||
| 135 | - Logger.debug(TAG, `on playPrevious `); | ||
| 136 | - }); | ||
| 137 | - | 177 | + // this.lastSession?.on('playNext', () => { |
| 178 | + // Logger.debug(TAG, `on playNext `); | ||
| 179 | + // }); | ||
| 180 | + // this.lastSession?.on('playPrevious', () => { | ||
| 181 | + // Logger.debug(TAG, `on playPrevious `); | ||
| 182 | + // }); | ||
| 138 | this.lastSession?.on('seek', (position: number) => { | 183 | this.lastSession?.on('seek', (position: number) => { |
| 139 | Logger.debug(TAG, `on seek , the time is ${JSON.stringify(position)}`); | 184 | Logger.debug(TAG, `on seek , the time is ${JSON.stringify(position)}`); |
| 140 | 185 | ||
| @@ -151,13 +196,16 @@ export class BackgroundAudioController { | @@ -151,13 +196,16 @@ export class BackgroundAudioController { | ||
| 151 | }); | 196 | }); |
| 152 | 197 | ||
| 153 | // 应用响应seek命令,使用应用内播放器完成seek实现 | 198 | // 应用响应seek命令,使用应用内播放器完成seek实现 |
| 154 | - | ||
| 155 | - | 199 | + this.avplayerController?.setSeekTime(position * 0.001, SliderChangeMode.End) |
| 200 | + this.hasSetupProgress = false | ||
| 156 | }); | 201 | }); |
| 157 | } | 202 | } |
| 158 | 203 | ||
| 159 | // 开启后台长时任务 | 204 | // 开启后台长时任务 |
| 160 | startContinuousTask() { | 205 | startContinuousTask() { |
| 206 | + if (this.applyedLongTaskPlay) { | ||
| 207 | + return | ||
| 208 | + } | ||
| 161 | let params: Record<string, string> = { | 209 | let params: Record<string, string> = { |
| 162 | "title": "开始后台任务", | 210 | "title": "开始后台任务", |
| 163 | "content": "内容?", | 211 | "content": "内容?", |
| @@ -175,9 +223,9 @@ export class BackgroundAudioController { | @@ -175,9 +223,9 @@ export class BackgroundAudioController { | ||
| 175 | deviceId: '', | 223 | deviceId: '', |
| 176 | bundleName: "com.peopledailychina.hosactivity", | 224 | bundleName: "com.peopledailychina.hosactivity", |
| 177 | abilityName: "EntryAbility", | 225 | abilityName: "EntryAbility", |
| 178 | - action: 'com.test.pushaction', | ||
| 179 | - entities: [], | ||
| 180 | - parameters:params, | 226 | + // action: 'com.test.pushaction', |
| 227 | + // entities: [], | ||
| 228 | + // parameters:params, | ||
| 181 | } | 229 | } |
| 182 | ], | 230 | ], |
| 183 | // 指定点击通知栏消息后的动作是拉起ability | 231 | // 指定点击通知栏消息后的动作是拉起ability |
| @@ -195,6 +243,7 @@ export class BackgroundAudioController { | @@ -195,6 +243,7 @@ export class BackgroundAudioController { | ||
| 195 | backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, | 243 | backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, |
| 196 | wantAgentObj).then(() => { | 244 | wantAgentObj).then(() => { |
| 197 | Logger.debug(TAG, `Succeeded in operationing startBackgroundRunning.`); | 245 | Logger.debug(TAG, `Succeeded in operationing startBackgroundRunning.`); |
| 246 | + this.applyedLongTaskPlay = true | ||
| 198 | }).catch((err: BusinessError) => { | 247 | }).catch((err: BusinessError) => { |
| 199 | Logger.error(TAG, `Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`); | 248 | Logger.error(TAG, `Failed to operation startBackgroundRunning. Code is ${err.code}, message is ${err.message}`); |
| 200 | }); | 249 | }); |
| @@ -208,6 +257,7 @@ export class BackgroundAudioController { | @@ -208,6 +257,7 @@ export class BackgroundAudioController { | ||
| 208 | }).catch((err: BusinessError) => { | 257 | }).catch((err: BusinessError) => { |
| 209 | Logger.error(TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`); | 258 | Logger.error(TAG, `Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`); |
| 210 | }); | 259 | }); |
| 260 | + this.applyedLongTaskPlay = false | ||
| 211 | } | 261 | } |
| 212 | 262 | ||
| 213 | } | 263 | } |
| @@ -24,6 +24,9 @@ | @@ -24,6 +24,9 @@ | ||
| 24 | "startWindowIcon": "$media:app_icon", | 24 | "startWindowIcon": "$media:app_icon", |
| 25 | "startWindowBackground": "$color:start_window_background", | 25 | "startWindowBackground": "$color:start_window_background", |
| 26 | "exported": true, | 26 | "exported": true, |
| 27 | + "backgroundModes": [ | ||
| 28 | + "audioPlayback" | ||
| 29 | + ], | ||
| 27 | "skills": [ | 30 | "skills": [ |
| 28 | { | 31 | { |
| 29 | "entities": [ | 32 | "entities": [ |
| @@ -33,6 +36,8 @@ | @@ -33,6 +36,8 @@ | ||
| 33 | "action.system.home", | 36 | "action.system.home", |
| 34 | "com.test.pushaction" | 37 | "com.test.pushaction" |
| 35 | ], | 38 | ], |
| 39 | + }, | ||
| 40 | + { | ||
| 36 | "uris": [ | 41 | "uris": [ |
| 37 | { | 42 | { |
| 38 | "scheme": 'rmrbapp', | 43 | "scheme": 'rmrbapp', |
| @@ -40,10 +45,9 @@ | @@ -40,10 +45,9 @@ | ||
| 40 | 'port': '8080', | 45 | 'port': '8080', |
| 41 | "path": 'openwith' | 46 | "path": 'openwith' |
| 42 | } | 47 | } |
| 43 | - ] | 48 | + ], |
| 44 | } | 49 | } |
| 45 | - ], | ||
| 46 | - "backgroundModes": ["audioPlayback"] | 50 | + ] |
| 47 | } | 51 | } |
| 48 | ], | 52 | ], |
| 49 | "metadata": [ | 53 | "metadata": [ |
-
Please register or login to post a comment